diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 000000000..f97596076 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,51 @@ +name: Build Docker Image + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: false + tags: python-mapnik:latest + cache-from: type=gha,scope=buildkit-python-mapnik + cache-to: type=gha,mode=max,scope=buildkit-python-mapnik + load: true + + - name: Cache uv virtual environment (for container tests) + uses: actions/cache@v5 + with: + path: .venv + key: venv-docker-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }} + restore-keys: | + venv-docker-${{ runner.os }}- + + - name: Run tests in Docker container + run: | + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace/test/python_tests \ + python-mapnik:latest \ + sh -c "uv sync --extra test && uv run pytest . -v" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..85b81bcc5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,136 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., v4.2.0)' + required: true + type: string + +jobs: + test: + uses: ./.github/workflows/test.yml + + build: + needs: test + runs-on: ubuntu-latest + container: + image: python:3.12-bookworm + env: + UV_LINK_MODE: copy + UV_CACHE_DIR: /github/home/.cache/uv + permissions: + actions: write + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Cache uv downloads + uses: actions/cache@v5 + with: + path: /github/home/.cache/uv + key: uvcache-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + restore-keys: | + uvcache-${{ runner.os }}- + + - name: Extract version from tag + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION=${{ inputs.tag }} + else + VERSION=${GITHUB_REF#refs/tags/} + fi + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: Add Debian sid repository + run: | + echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list + echo 'Package: *\nPin: release a=sid\nPin-Priority: 100' > /etc/apt/preferences.d/sid + + - name: Install system dependencies + run: | + apt-get update + # Ensure /etc/os-release exists (required by some actions' OS detection). + apt-get install -y base-files + apt-get install -y build-essential pkg-config libbz2-dev + apt-get install -y -t sid libmapnik-dev + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: false + + - name: Build wheel and sdist + run: | + uv build --wheel --sdist + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + if: github.event_name == 'push' + with: + tag_name: ${{ steps.version.outputs.version }} + name: Release ${{ steps.version.outputs.version }} + body: | + ## Python Mapnik ${{ steps.version.outputs.version_number }} + + ### Installation + + Download the wheel file below and install: + ```bash + pip install mapnik-${{ steps.version.outputs.version_number }}-*.whl + ``` + + Or install directly from GitHub: + ```bash + pip install https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.version }}/mapnik-${{ steps.version.outputs.version_number }}-cp312-cp312-linux_x86_64.whl + ``` + + Or install from source: + ```bash + pip install git+https://github.com/${{ github.repository }}.git@${{ steps.version.outputs.version }} + ``` + + ### Requirements + + - Python >= 3.8 + - Mapnik >= 4.0.0 + + ### Changes + + See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${{ steps.version.outputs.version }}/CHANGELOG.md) for details. + files: | + dist/*.whl + dist/*.tar.gz + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Summary + run: | + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ Released version: **${{ steps.version.outputs.version }}**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Artifacts Built:" >> $GITHUB_STEP_SUMMARY + ls -lh dist/ >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Distribution:" >> $GITHUB_STEP_SUMMARY + echo "- GitHub Release: ✅" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..b32a9a160 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,111 @@ +name: Test + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + workflow_dispatch: + workflow_call: + +jobs: + test: + runs-on: ubuntu-latest + container: + image: python:3.12-bookworm + env: + UV_LINK_MODE: copy + UV_CACHE_DIR: /github/home/.cache/uv + permissions: + actions: write + contents: read + checks: write + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Cache uv downloads + uses: actions/cache@v5 + with: + path: /github/home/.cache/uv + key: uvcache-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }} + restore-keys: | + uvcache-${{ runner.os }}- + + - name: Cache uv virtual environment and build artifacts + uses: actions/cache@v5 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}-${{ hashFiles('src/**/*.cpp', 'src/**/*.hpp', 'packaging/**/*.py') }} + restore-keys: | + venv-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml', 'setup.py') }}- + venv-${{ runner.os }}- + + - name: Add Debian sid repository + run: | + echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list + echo 'Package: *\nPin: release a=sid\nPin-Priority: 100' > /etc/apt/preferences.d/sid + + - name: Install system dependencies + run: | + apt-get update + # Ensure /etc/os-release exists (required by some actions' OS detection). + apt-get install -y base-files + apt-get install -y build-essential pkg-config libbz2-dev + apt-get install -y -t sid libmapnik-dev fonts-noto-cjk + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + # Work around container-job failures determining Linux distribution during + # setup-uv's built-in caching path; we cache UV_CACHE_DIR ourselves above. + enable-cache: false + + - name: Install Python dependencies and build package + run: | + # Use lockfile when present; avoid doing two full syncs (which can rebuild twice). + if [ -f uv.lock ]; then + uv sync --extra test --frozen --verbose + else + uv sync --extra test --verbose + fi + + - name: Run tests with coverage + run: | + uv run pytest test/python_tests -v \ + --cov=mapnik \ + --cov-report=term \ + --cov-report=xml:coverage.xml \ + --cov-report=html:htmlcov \ + --junitxml=junit.xml + + - name: Upload coverage reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-reports + path: | + coverage.xml + htmlcov/ + junit.xml + retention-days: 30 + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: junit.xml + check_name: Test Results + comment_mode: off + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + if: always() + with: + files: ./coverage.xml + flags: unittests + name: python-mapnik-coverage + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 331398c60..3de314f6a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.dylib build/ dist/ -mapnik/paths.py +packaging/mapnik/paths.py *.egg-info/ .eggs/ .mason/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2dde7fe75 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.14-rc-bookworm + +# Build-time proxy support (affects apt/uv/etc. during docker build) +ARG http_proxy +ARG https_proxy +ARG no_proxy +ENV http_proxy=${http_proxy} \ + https_proxy=${https_proxy} \ + no_proxy=${no_proxy} \ + HTTP_PROXY=${http_proxy} \ + HTTPS_PROXY=${https_proxy} \ + NO_PROXY=${no_proxy} + +# 添加 Debian sid 仓库以获取 Mapnik 4.2 +RUN echo "deb http://deb.debian.org/debian sid main" >> /etc/apt/sources.list.d/sid.list && \ + echo 'Package: *\nPin: release a=sid\nPin-Priority: 100' > /etc/apt/preferences.d/sid + +# 安装必要的工具 +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + pkg-config + +# 安装 Mapnik 4.2 和必要的开发工具 +RUN apt-get install -y -t sid \ + libmapnik-dev \ + fonts-noto-cjk + +# 需要额外的依赖写在这里,避免缓存失效。上面的依赖安装起来时间很长 +RUN apt-get install -y \ + libbz2-dev \ + && rm -rf /var/lib/apt/lists/* + +# 安装 uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# 复制依赖文件 +COPY pyproject.toml . +COPY uv.lock* ./ + +# 复制源代码文件(构建本地包所需) +COPY src/ src/ +COPY setup.py . +COPY build.py . +COPY packaging/ packaging/ + +# 安装 Python 依赖(包括构建本地 python-mapnik) +# 使用 --verbose 显示详细的编译日志 +RUN uv sync --frozen --no-dev --verbose || uv sync --no-dev --verbose diff --git a/README.md b/README.md index 1324e021f..fd5b51a79 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ Once you have installed you can test the package by running: pytest test/python_tests/ ``` +## UV Sync via Docker - - +``` +docker run --rm -it \ + -e http_proxy -e https_proxy -e no_proxy \ + -v "$(pwd):/workspace" \ + -w /workspace \ + python-mapnik:local \ + uv sync --verbose +``` diff --git a/build.py b/build.py index 0f94826b6..2f757bd88 100644 --- a/build.py +++ b/build.py @@ -1,7 +1,7 @@ import glob import os from subprocess import Popen, PIPE -from distutils import sysconfig +import sysconfig Import('env') @@ -10,15 +10,15 @@ def call(cmd, silent=True): if not stderr: return stdin.strip() elif not silent: - print stderr + print(stderr) prefix = env['PREFIX'] -target_path = os.path.normpath(sysconfig.get_python_lib() + os.path.sep + env['MAPNIK_NAME']) +target_path = os.path.normpath(sysconfig.get_path('purelib') + os.path.sep + env['MAPNIK_NAME']) py_env = env.Clone() -py_env.Append(CPPPATH = sysconfig.get_python_inc()) +py_env.Append(CPPPATH = sysconfig.get_path('include')) py_env.Append(CPPDEFINES = env['LIBMAPNIK_DEFINES']) @@ -63,12 +63,12 @@ def call(cmd, silent=True): if not os.path.exists(env['MAPNIK_NAME']): os.mkdir(env['MAPNIK_NAME']) -file('mapnik/paths.py','w').write(paths % (env['MAPNIK_LIB_DIR'])) +open('mapnik/paths.py', 'w', encoding='utf-8').write(paths % (env['MAPNIK_LIB_DIR'])) # force open perms temporarily so that `sudo scons install` # does not later break simple non-install non-sudo rebuild try: - os.chmod('mapnik/paths.py',0666) + os.chmod('mapnik/paths.py', 0o666) except: pass # install the shared object beside the module directory @@ -89,7 +89,7 @@ def call(cmd, silent=True): env.Command( targetp, 'mapnik/paths.py', [ Copy("$TARGET","$SOURCE"), - Chmod("$TARGET", 0644), + Chmod("$TARGET", 0o644), ]) if 'uninstall' not in COMMAND_LINE_TARGETS: diff --git a/packaging/mapnik/__init__.py b/packaging/mapnik/__init__.py index 1fb21c7ea..99ba47046 100644 --- a/packaging/mapnik/__init__.py +++ b/packaging/mapnik/__init__.py @@ -38,6 +38,7 @@ import itertools import os +import sys import warnings def bootstrap_env(): @@ -63,6 +64,34 @@ def bootstrap_env(): bootstrap_env() +# In some build/test setups the compiled extension can be imported under the +# top-level name `_mapnik` (e.g. due to sys.path/build output layout) and then +# again as `mapnik._mapnik`. Loading the same pybind11 extension twice in one +# interpreter can raise "already registered" errors for pybind11 types/enums. +# +# To avoid a second load, if `_mapnik` is already present, alias it to the +# canonical `mapnik._mapnik` name before importing. +_canonical_ext_name = f"{__name__}._mapnik" +if _canonical_ext_name not in sys.modules and "_mapnik" in sys.modules: + # If a top-level `_mapnik` was imported first, alias it to the canonical + # `mapnik._mapnik` name so Python reuses the same extension module object + # rather than attempting a second load. + sys.modules[_canonical_ext_name] = sys.modules["_mapnik"] + +_prev_err = getattr(sys, "_python_mapnik_ext_import_error", None) +if _prev_err is not None: + # Avoid repeated attempts to load the extension in the same interpreter + # after a failure (which can lead to confusing secondary errors like + # "already registered" from pybind11). + raise ImportError(str(_prev_err)) from None + +try: + from . import _mapnik as _ext +except ImportError as e: + setattr(sys, "_python_mapnik_ext_import_error", e) + raise +# Ensure subsequent `import _mapnik` reuses the already-loaded extension. +sys.modules.setdefault("_mapnik", _ext) from ._mapnik import * def Shapefile(**keywords): diff --git a/pyproject.toml b/pyproject.toml index 4a8ff0b02..c688ff117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "mapnik" -version = "4.1.3.beta" +version = "4.2.0" description = "Python bindings for Mapnik" license = "LGPL-2.1-or-later" @@ -22,7 +22,13 @@ maintainers = [ {name= "Artem Pavlenko", email = "artem@mapnik.org"}, ] -requires-python = ">= 3.8" +requires-python = ">= 3.12" + +[project.optional-dependencies] +test = [ + "pytest >= 8.0", + "pytest-cov >= 5.0", +] [project.urls] Homepage = "https://mapnik.org" @@ -35,4 +41,8 @@ Changelog = "https://github.com/mapnik/python-mapnik/blob/master/CHANGELOG.md" minversion = "8.0" testpaths = [ "test/python_tests", -] \ No newline at end of file +] + +[tool.uv] +# Pin uv version for CI/dev consistency (used by astral-sh/setup-uv). +required-version = ">=0.9.0" \ No newline at end of file diff --git a/setup.py b/setup.py index 3a26ece45..360cb6aa2 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #! /usr/bin/env python3 import os +import shlex import subprocess import sys @@ -8,49 +9,144 @@ from setuptools import find_packages, setup -def check_output(args): - output = subprocess.check_output(args).decode() - return output.rstrip("\n") +class MapnikBuildConfig: + """ + Centralizes build configuration for the Mapnik pybind11 extension. + Keep this file import-safe for setuptools/pip: do all external discovery + (pkg-config) inside this class, not at module import time. + """ -linkflags = [] -lib_path = os.path.join( - check_output(["pkg-config", "--variable=prefix", "libmapnik"]), "lib" -) -linkflags.extend(check_output(["pkg-config", "--libs", "libmapnik"]).split(" ")) - -# Dynamically make the mapnik/paths.py file -f_paths = open("packaging/mapnik/paths.py", "w") -f_paths.write("import os\n") -f_paths.write("\n") - -input_plugin_path = check_output(["pkg-config", "--variable=plugins_dir", "libmapnik"]) -font_path = check_output(["pkg-config", "--variable=fonts_dir", "libmapnik"]) - -if os.environ.get("LIB_DIR_NAME"): - mapnik_lib_path = lib_path + os.environ.get("LIB_DIR_NAME") -else: - mapnik_lib_path = lib_path + "/mapnik" - f_paths.write("mapniklibpath = '{path}'\n".format(path=mapnik_lib_path)) - f_paths.write("inputpluginspath = '{path}'\n".format(path=input_plugin_path)) - f_paths.write("fontscollectionpath = '{path}'\n".format(path=font_path)) - f_paths.write("__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n") - f_paths.close() - -extra_comp_args = check_output(["pkg-config", "--cflags", "libmapnik"]).split(" ") -extra_comp_args = list( - filter(lambda arg: arg != "-fvisibility=hidden", extra_comp_args) -) + def __init__(self, pkg_name: str = "libmapnik"): + self.pkg_name = pkg_name + self.linkflags: list[str] = [] + self.extra_comp_args: list[str] = [] + self.mapnik_lib_path: str = "" + self.input_plugin_path: str = "" + self.font_path: str = "" + + @staticmethod + def _check_output(args: list[str]) -> str: + output = subprocess.check_output(args).decode() + return output.rstrip("\n") + + def _pkg_config_var(self, var_name: str) -> str: + return self._check_output(["pkg-config", "--variable=" + var_name, self.pkg_name]) + + @staticmethod + def _split_flags(s: str) -> list[str]: + # pkg-config/mapnik-config return shell-like strings; shlex handles quoted paths safely. + return [arg for arg in shlex.split(s) if arg] + + def _ensure_cpp_std(self) -> None: + """ + Some environments don't inject a C++ standard in pkg-config/mapnik-config flags. + This project uses C++17 features (e.g., `template `), so ensure C++17. + """ + # Always force C++17 as the last -std flag so it wins over older standards. + # (GCC/Clang take the last -std=... argument.) + if not any(arg.startswith("/std:") for arg in self.extra_comp_args): + self.extra_comp_args.append("-std=c++17") + + def _discover_with_pkg_config(self) -> None: + # Linker flags / library location. + prefix = self._pkg_config_var("prefix") + lib_path = os.path.join(prefix, "lib") + self.linkflags.extend(self._split_flags(self._check_output(["pkg-config", "--libs", self.pkg_name]))) + + # Runtime data locations. + self.input_plugin_path = self._pkg_config_var("plugins_dir") + self.font_path = self._pkg_config_var("fonts_dir") + + lib_dir_name = os.environ.get("LIB_DIR_NAME") + if lib_dir_name: + self.mapnik_lib_path = lib_path + lib_dir_name + else: + self.mapnik_lib_path = lib_path + "/mapnik" + + # Compiler flags. + extra_comp_args = self._split_flags(self._check_output(["pkg-config", "--cflags", self.pkg_name])) + self.extra_comp_args = [arg for arg in extra_comp_args if arg != "-fvisibility=hidden"] + + def _mapnik_config(self) -> str: + # Allow pinning a specific mapnik-config (useful in CI / non-standard prefixes). + return os.environ.get("MAPNIK_CONFIG", "mapnik-config") + + def _mapnik_config_try_flag(self, flag_candidates: list[str]) -> str: + """ + Try mapnik-config with one of the provided flags and return the first + successful (non-empty) output. Returns "" if none work. + """ + cmd = self._mapnik_config() + for flag in flag_candidates: + try: + out = self._check_output([cmd, flag]).strip() + except (FileNotFoundError, subprocess.CalledProcessError): + continue + if out: + return out + return "" + + def _discover_with_mapnik_config(self) -> None: + cmd = self._mapnik_config() + prefix = self._check_output([cmd, "--prefix"]) + lib_path = os.path.join(prefix, "lib") + + # flags + self.linkflags.extend(self._split_flags(self._check_output([cmd, "--libs"]))) + extra_comp_args = self._split_flags(self._check_output([cmd, "--cflags"])) + self.extra_comp_args = [arg for arg in extra_comp_args if arg != "-fvisibility=hidden"] + + # runtime locations (best-effort: flags vary slightly across mapnik versions/distros) + self.input_plugin_path = self._mapnik_config_try_flag( + ["--input-plugins", "--input-plugins-dir", "--input-plugins-path"] + ) + self.font_path = self._mapnik_config_try_flag(["--fonts", "--fonts-dir", "--fonts-path"]) + + lib_dir_name = os.environ.get("LIB_DIR_NAME") + if lib_dir_name: + self.mapnik_lib_path = lib_path + lib_dir_name + else: + self.mapnik_lib_path = lib_path + "/mapnik" + + def discover(self) -> None: + # Prefer pkg-config, but fall back to mapnik-config (common on some distros/builds). + try: + self._discover_with_pkg_config() + except (FileNotFoundError, subprocess.CalledProcessError): + self._discover_with_mapnik_config() + + if not self.input_plugin_path or not self.font_path: + raise RuntimeError( + "Failed to discover Mapnik runtime paths. " + "Tried pkg-config variables (plugins_dir/fonts_dir) and mapnik-config flags " + "(--input-plugins/--fonts). You can set MAPNIK_CONFIG to point to mapnik-config." + ) + + self._ensure_cpp_std() + + # Platform-specific linker flags. + if sys.platform != "darwin": + self.linkflags.append("-lrt") + self.linkflags.append("-Wl,-z,origin") + self.linkflags.append("-Wl,-rpath=$ORIGIN/lib") + + self.linkflags = [arg for arg in self.linkflags if arg] + + def write_paths_py(self, target_file: str = "packaging/mapnik/paths.py") -> None: + os.makedirs(os.path.dirname(target_file), exist_ok=True) + with open(target_file, "w", encoding="utf-8") as f_paths: + f_paths.write("import os\n\n") + f_paths.write(f"mapniklibpath = {self.mapnik_lib_path!r}\n") + f_paths.write(f"inputpluginspath = {self.input_plugin_path!r}\n") + f_paths.write(f"fontscollectionpath = {self.font_path!r}\n") + # __all__ should be a list of names (strings), not the values. + f_paths.write('__all__ = ["mapniklibpath", "inputpluginspath", "fontscollectionpath"]\n') -if sys.platform == "darwin": - pass -else: - linkflags.append("-lrt") - linkflags.append("-Wl,-z,origin") - linkflags.append("-Wl,-rpath=$ORIGIN/lib") -extra_comp_args = list(filter(lambda arg: arg != "", extra_comp_args)) -linkflags = list(filter(lambda arg: arg != "", linkflags)) +cfg = MapnikBuildConfig("libmapnik") +cfg.discover() +cfg.write_paths_py() ext_modules = [ Pybind11Extension( @@ -104,8 +200,9 @@ def check_output(args): "src/mapnik_shield_symbolizer.cpp", "src/mapnik_group_symbolizer.cpp", ], - extra_compile_args=extra_comp_args, - extra_link_args=linkflags, + cxx_std=17, + extra_compile_args=cfg.extra_comp_args, + extra_link_args=cfg.linkflags, ) ] @@ -116,7 +213,7 @@ def check_output(args): setup( name="mapnik", - version="4.0.0.dev", + version="4.2.0.dev", packages=find_packages(where="packaging"), package_dir={"": "packaging"}, package_data={ diff --git a/src/mapnik_coord.cpp b/src/mapnik_coord.cpp index 93249c34c..7701b6365 100644 --- a/src/mapnik_coord.cpp +++ b/src/mapnik_coord.cpp @@ -41,7 +41,14 @@ void export_coord(py::module const& m) "Gets or sets the x/lon coordinate of the point.\n") .def_readwrite("y", &coord::y, "Gets or sets the y/lat coordinate of the point.\n") - .def(py::self == py::self) // __eq__ + // mapnik::coord::operator== is non-const in some Mapnik versions, + // which breaks pybind11's operator helper (it compares `const` values). + // Define __eq__ explicitly to avoid relying on the C++ operator signature. + .def("__eq__", + [](coord const& a, coord const& b) { + return a.x == b.x && a.y == b.y; + }, + py::is_operator()) .def(py::self + py::self) //__add__ .def(py::self + float()) .def(float() + py::self) diff --git a/src/mapnik_font_engine.cpp b/src/mapnik_font_engine.cpp index a461241b1..0cad1592d 100644 --- a/src/mapnik_font_engine.cpp +++ b/src/mapnik_font_engine.cpp @@ -32,9 +32,26 @@ void export_font_engine(py::module const& m) { using mapnik::freetype_engine; - py::class_(m, "FontEngine") - .def_static("register_font", &freetype_engine::register_font) - .def_static("register_fonts", &freetype_engine::register_fonts) - .def_static("face_names", &freetype_engine::face_names) - ; + // Keep bindings intentionally simple: some CI environments have hit + // "Internal error while parsing type signature" during import. Avoid + // default args / keyword-only metadata here and expose explicit overloads. + auto cls = py::class_(m, "FontEngine"); + + cls.def_static("register_font", + [](std::string const& file_name) { return freetype_engine::register_font(file_name); }); + + cls.def_static("register_fonts", + [](std::string const& dir) { return freetype_engine::register_fonts(dir, false); }); + cls.def_static("register_fonts", + [](std::string const& dir, bool recurse) { return freetype_engine::register_fonts(dir, recurse); }); + + // Avoid exposing std::vector directly in the binding signature. + cls.def_static("face_names", []() { + py::list out; + for (auto const& name : freetype_engine::face_names()) + { + out.append(name); + } + return out; + }); } diff --git a/src/mapnik_map.cpp b/src/mapnik_map.cpp index 895787dee..b2b164339 100644 --- a/src/mapnik_map.cpp +++ b/src/mapnik_map.cpp @@ -295,7 +295,7 @@ void export_map(py::module const& m) "\n" ">>> featureset.features\n" ">>> []\n", - py::arg("layer idx"), py::arg("x"), py::arg("y") + py::arg("layer_idx"), py::arg("x"), py::arg("y") ) .def("remove_all", &Map::remove_all, diff --git a/src/mapnik_python.cpp b/src/mapnik_python.cpp index 5a9bf1326..0358423a3 100644 --- a/src/mapnik_python.cpp +++ b/src/mapnik_python.cpp @@ -20,7 +20,7 @@ * *****************************************************************************/ -//mapnik +// mapnik #include #include #include @@ -52,633 +52,704 @@ #include #endif -//stl +// stl #include #include -//pybind11 +// pybind11 #include +#include namespace py = pybind11; -namespace { -void clear_cache() +namespace { - mapnik::marker_cache::instance().clear(); -#if defined(SHAPE_MEMORY_MAPPED_FILE) - mapnik::mapped_memory_cache::instance().clear(); -#endif -} - -struct agg_renderer_visitor_1 -{ - agg_renderer_visitor_1(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y) - : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} + template + void safe_export(char const *name, Fn &&fn) + { + try + { + fn(); + } + catch (py::error_already_set const &e) + { + throw std::runtime_error(std::string("pybind11 export failed in ") + name + ": " + e.what()); + } + catch (std::exception const &e) + { + throw std::runtime_error(std::string("pybind11 export failed in ") + name + ": " + e.what()); + } + catch (...) + { + throw std::runtime_error(std::string("pybind11 export failed in ") + name + ": unknown error"); + } + } - template - void operator() (T & pixmap) + void clear_cache() { - throw std::runtime_error("This image type is not currently supported for rendering."); + mapnik::marker_cache::instance().clear(); +#if defined(SHAPE_MEMORY_MAPPED_FILE) + mapnik::mapped_memory_cache::instance().clear(); +#endif } - private: - mapnik::Map const& m_; - double scale_factor_; - unsigned offset_x_; - unsigned offset_y_; -}; + struct agg_renderer_visitor_1 + { + agg_renderer_visitor_1(mapnik::Map const &m, double scale_factor, unsigned offset_x, unsigned offset_y) + : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} -template <> -void agg_renderer_visitor_1::operator() (mapnik::image_rgba8 & pixmap) -{ - mapnik::agg_renderer ren(m_,pixmap,scale_factor_,offset_x_, offset_y_); - ren.apply(); -} + template + void operator()(T &pixmap) + { + throw std::runtime_error("This image type is not currently supported for rendering."); + } -struct agg_renderer_visitor_2 -{ - agg_renderer_visitor_2(mapnik::Map const &m, std::shared_ptr detector, - double scale_factor, unsigned offset_x, unsigned offset_y) - : m_(m), detector_(detector), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} + private: + mapnik::Map const &m_; + double scale_factor_; + unsigned offset_x_; + unsigned offset_y_; + }; - template - void operator() (T & pixmap) + template <> + void agg_renderer_visitor_1::operator()(mapnik::image_rgba8 &pixmap) { - throw std::runtime_error("This image type is not currently supported for rendering."); + mapnik::agg_renderer ren(m_, pixmap, scale_factor_, offset_x_, offset_y_); + ren.apply(); } - private: - mapnik::Map const& m_; - std::shared_ptr detector_; - double scale_factor_; - unsigned offset_x_; - unsigned offset_y_; -}; + struct agg_renderer_visitor_2 + { + agg_renderer_visitor_2(mapnik::Map const &m, std::shared_ptr detector, + double scale_factor, unsigned offset_x, unsigned offset_y) + : m_(m), detector_(detector), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} -template <> -void agg_renderer_visitor_2::operator() (mapnik::image_rgba8 & pixmap) -{ - mapnik::agg_renderer ren(m_,pixmap,detector_, scale_factor_,offset_x_, offset_y_); - ren.apply(); -} + template + void operator()(T &pixmap) + { + throw std::runtime_error("This image type is not currently supported for rendering."); + } -struct agg_renderer_visitor_3 -{ - agg_renderer_visitor_3(mapnik::Map const& m, mapnik::request const& req, mapnik::attributes const& vars, - double scale_factor, unsigned offset_x, unsigned offset_y) - : m_(m), req_(req), vars_(vars), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} + private: + mapnik::Map const &m_; + std::shared_ptr detector_; + double scale_factor_; + unsigned offset_x_; + unsigned offset_y_; + }; - template - void operator() (T & pixmap) + template <> + void agg_renderer_visitor_2::operator()(mapnik::image_rgba8 &pixmap) { - throw std::runtime_error("This image type is not currently supported for rendering."); + mapnik::agg_renderer ren(m_, pixmap, detector_, scale_factor_, offset_x_, offset_y_); + ren.apply(); } - private: - mapnik::Map const& m_; - mapnik::request const& req_; - mapnik::attributes const& vars_; - double scale_factor_; - unsigned offset_x_; - unsigned offset_y_; - -}; - -template <> -void agg_renderer_visitor_3::operator() (mapnik::image_rgba8 & pixmap) -{ - mapnik::agg_renderer ren(m_,req_, vars_, pixmap, scale_factor_, offset_x_, offset_y_); - ren.apply(); -} + struct agg_renderer_visitor_3 + { + agg_renderer_visitor_3(mapnik::Map const &m, mapnik::request const &req, mapnik::attributes const &vars, + double scale_factor, unsigned offset_x, unsigned offset_y) + : m_(m), req_(req), vars_(vars), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y) {} -struct agg_renderer_visitor_4 -{ - agg_renderer_visitor_4(mapnik::Map const& m, double scale_factor, unsigned offset_x, unsigned offset_y, - mapnik::layer const& layer, std::set& names) - : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y), - layer_(layer), names_(names) {} + template + void operator()(T &pixmap) + { + throw std::runtime_error("This image type is not currently supported for rendering."); + } - template - void operator() (T & pixmap) + private: + mapnik::Map const &m_; + mapnik::request const &req_; + mapnik::attributes const &vars_; + double scale_factor_; + unsigned offset_x_; + unsigned offset_y_; + }; + + template <> + void agg_renderer_visitor_3::operator()(mapnik::image_rgba8 &pixmap) { - throw std::runtime_error("This image type is not currently supported for rendering."); + mapnik::agg_renderer ren(m_, req_, vars_, pixmap, scale_factor_, offset_x_, offset_y_); + ren.apply(); } - private: - mapnik::Map const& m_; - double scale_factor_; - unsigned offset_x_; - unsigned offset_y_; - mapnik::layer const& layer_; - std::set & names_; -}; - -template <> -void agg_renderer_visitor_4::operator() (mapnik::image_rgba8 & pixmap) -{ - mapnik::agg_renderer ren(m_,pixmap,scale_factor_,offset_x_, offset_y_); - ren.apply(layer_, names_); -} + struct agg_renderer_visitor_4 + { + agg_renderer_visitor_4(mapnik::Map const &m, double scale_factor, unsigned offset_x, unsigned offset_y, + mapnik::layer const &layer, std::set &names) + : m_(m), scale_factor_(scale_factor), offset_x_(offset_x), offset_y_(offset_y), + layer_(layer), names_(names) {} + template + void operator()(T &pixmap) + { + throw std::runtime_error("This image type is not currently supported for rendering."); + } -void render(mapnik::Map const& map, - mapnik::image_any& image, - double scale_factor = 1.0, - unsigned offset_x = 0u, - unsigned offset_y = 0u) -{ - py::gil_scoped_release release; - mapnik::util::apply_visitor(agg_renderer_visitor_1(map, scale_factor, offset_x, offset_y), image); -} + private: + mapnik::Map const &m_; + double scale_factor_; + unsigned offset_x_; + unsigned offset_y_; + mapnik::layer const &layer_; + std::set &names_; + }; + + template <> + void agg_renderer_visitor_4::operator()(mapnik::image_rgba8 &pixmap) + { + mapnik::agg_renderer ren(m_, pixmap, scale_factor_, offset_x_, offset_y_); + ren.apply(layer_, names_); + } -void render_with_vars(mapnik::Map const& map, - mapnik::image_any& image, - py::dict const& d, - double scale_factor = 1.0, - unsigned offset_x = 0u, - unsigned offset_y = 0u) -{ - mapnik::attributes vars = mapnik::dict2attr(d); - mapnik::request req(map.width(),map.height(),map.get_current_extent()); - req.set_buffer_size(map.buffer_size()); - py::gil_scoped_release release; - mapnik::util::apply_visitor(agg_renderer_visitor_3(map, req, vars, scale_factor, offset_x, offset_y), image); -} + void render(mapnik::Map const &map, + mapnik::image_any &image, + double scale_factor = 1.0, + unsigned offset_x = 0u, + unsigned offset_y = 0u) + { + py::gil_scoped_release release; + mapnik::util::apply_visitor(agg_renderer_visitor_1(map, scale_factor, offset_x, offset_y), image); + } -void render_with_detector( - mapnik::Map const& map, - mapnik::image_any &image, - std::shared_ptr detector, - double scale_factor = 1.0, - unsigned offset_x = 0u, - unsigned offset_y = 0u) -{ - py::gil_scoped_release release; - mapnik::util::apply_visitor(agg_renderer_visitor_2(map, detector, scale_factor, offset_x, offset_y), image); -} + void render_with_vars(mapnik::Map const &map, + mapnik::image_any &image, + py::dict const &d, + double scale_factor = 1.0, + unsigned offset_x = 0u, + unsigned offset_y = 0u) + { + mapnik::attributes vars = mapnik::dict2attr(d); + mapnik::request req(map.width(), map.height(), map.get_current_extent()); + req.set_buffer_size(map.buffer_size()); + py::gil_scoped_release release; + mapnik::util::apply_visitor(agg_renderer_visitor_3(map, req, vars, scale_factor, offset_x, offset_y), image); + } -void render_layer2(mapnik::Map const& map, - mapnik::image_any& image, - unsigned layer_idx, - double scale_factor, - unsigned offset_x, - unsigned offset_y) -{ - std::vector const& layers = map.layers(); - std::size_t layer_num = layers.size(); - if (layer_idx >= layer_num) { - std::ostringstream s; - s << "Zero-based layer index '" << layer_idx << "' not valid, only '" - << layer_num << "' layers are in map\n"; - throw std::runtime_error(s.str()); + void render_with_detector( + mapnik::Map const &map, + mapnik::image_any &image, + std::shared_ptr detector, + double scale_factor = 1.0, + unsigned offset_x = 0u, + unsigned offset_y = 0u) + { + py::gil_scoped_release release; + mapnik::util::apply_visitor(agg_renderer_visitor_2(map, detector, scale_factor, offset_x, offset_y), image); } - py::gil_scoped_release release; - mapnik::layer const& layer = layers[layer_idx]; - std::set names; - mapnik::util::apply_visitor(agg_renderer_visitor_4(map, scale_factor, offset_x, offset_y, layer, names), image); -} + void render_layer2(mapnik::Map const &map, + mapnik::image_any &image, + unsigned layer_idx, + double scale_factor, + unsigned offset_x, + unsigned offset_y) + { + std::vector const &layers = map.layers(); + std::size_t layer_num = layers.size(); + if (layer_idx >= layer_num) + { + std::ostringstream s; + s << "Zero-based layer index '" << layer_idx << "' not valid, only '" + << layer_num << "' layers are in map\n"; + throw std::runtime_error(s.str()); + } + + py::gil_scoped_release release; + mapnik::layer const &layer = layers[layer_idx]; + std::set names; + mapnik::util::apply_visitor(agg_renderer_visitor_4(map, scale_factor, offset_x, offset_y, layer, names), image); + } #if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO) -void render3(mapnik::Map const& map, - PycairoSurface* py_surface, - double scale_factor = 1.0, - unsigned offset_x = 0, - unsigned offset_y = 0) -{ - py::gil_scoped_release release; - mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); - mapnik::cairo_renderer ren(map,mapnik::create_context(surface),scale_factor,offset_x,offset_y); - ren.apply(); -} + void render3(mapnik::Map const &map, + PycairoSurface *py_surface, + double scale_factor = 1.0, + unsigned offset_x = 0, + unsigned offset_y = 0) + { + py::gil_scoped_release release; + mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); + mapnik::cairo_renderer ren(map, mapnik::create_context(surface), scale_factor, offset_x, offset_y); + ren.apply(); + } -void render4(mapnik::Map const& map, PycairoSurface* py_surface) -{ - py::gil_scoped_release release; - mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); - mapnik::cairo_renderer ren(map,mapnik::create_context(surface)); - ren.apply(); -} + void render4(mapnik::Map const &map, PycairoSurface *py_surface) + { + py::gil_scoped_release release; + mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); + mapnik::cairo_renderer ren(map, mapnik::create_context(surface)); + ren.apply(); + } -void render5(mapnik::Map const& map, - PycairoContext* py_context, - double scale_factor = 1.0, - unsigned offset_x = 0, - unsigned offset_y = 0) -{ - py::gil_scoped_release release; - mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); - mapnik::cairo_renderer ren(map,context,scale_factor,offset_x, offset_y); - ren.apply(); -} + void render5(mapnik::Map const &map, + PycairoContext *py_context, + double scale_factor = 1.0, + unsigned offset_x = 0, + unsigned offset_y = 0) + { + py::gil_scoped_release release; + mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); + mapnik::cairo_renderer ren(map, context, scale_factor, offset_x, offset_y); + ren.apply(); + } -void render6(mapnik::Map const& map, PycairoContext* py_context) -{ - py::gil_scoped_release release; - mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); - mapnik::cairo_renderer ren(map,context); - ren.apply(); -} -void render_with_detector2( - mapnik::Map const& map, - PycairoContext* py_context, - std::shared_ptr detector) -{ - py::gil_scoped_release release; - mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); - mapnik::cairo_renderer ren(map,context,detector); - ren.apply(); -} + void render6(mapnik::Map const &map, PycairoContext *py_context) + { + py::gil_scoped_release release; + mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); + mapnik::cairo_renderer ren(map, context); + ren.apply(); + } + void render_with_detector2( + mapnik::Map const &map, + PycairoContext *py_context, + std::shared_ptr detector) + { + py::gil_scoped_release release; + mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); + mapnik::cairo_renderer ren(map, context, detector); + ren.apply(); + } -void render_with_detector3( - mapnik::Map const& map, - PycairoContext* py_context, - std::shared_ptr detector, - double scale_factor = 1.0, - unsigned offset_x = 0u, - unsigned offset_y = 0u) -{ - py::gil_scoped_release release; - mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); - mapnik::cairo_renderer ren(map,context,detector,scale_factor,offset_x,offset_y); - ren.apply(); -} + void render_with_detector3( + mapnik::Map const &map, + PycairoContext *py_context, + std::shared_ptr detector, + double scale_factor = 1.0, + unsigned offset_x = 0u, + unsigned offset_y = 0u) + { + py::gil_scoped_release release; + mapnik::cairo_ptr context(cairo_reference(py_context->ctx), mapnik::cairo_closer()); + mapnik::cairo_renderer ren(map, context, detector, scale_factor, offset_x, offset_y); + ren.apply(); + } -void render_with_detector4( - mapnik::Map const& map, - PycairoSurface* py_surface, - std::shared_ptr detector) -{ - py::gil_scoped_release release; - mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); - mapnik::cairo_renderer ren(map, mapnik::create_context(surface), detector); - ren.apply(); -} + void render_with_detector4( + mapnik::Map const &map, + PycairoSurface *py_surface, + std::shared_ptr detector) + { + py::gil_scoped_release release; + mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); + mapnik::cairo_renderer ren(map, mapnik::create_context(surface), detector); + ren.apply(); + } -void render_with_detector5( - mapnik::Map const& map, - PycairoSurface* py_surface, - std::shared_ptr detector, - double scale_factor = 1.0, - unsigned offset_x = 0u, - unsigned offset_y = 0u) -{ - py::gil_scoped_release release; - mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); - mapnik::cairo_renderer ren(map, mapnik::create_context(surface), detector, scale_factor, offset_x, offset_y); - ren.apply(); -} + void render_with_detector5( + mapnik::Map const &map, + PycairoSurface *py_surface, + std::shared_ptr detector, + double scale_factor = 1.0, + unsigned offset_x = 0u, + unsigned offset_y = 0u) + { + py::gil_scoped_release release; + mapnik::cairo_surface_ptr surface(cairo_surface_reference(py_surface->surface), mapnik::cairo_surface_closer()); + mapnik::cairo_renderer ren(map, mapnik::create_context(surface), detector, scale_factor, offset_x, offset_y); + ren.apply(); + } #endif + void render_tile_to_file(mapnik::Map const &map, + unsigned offset_x, unsigned offset_y, + unsigned width, unsigned height, + std::string const &file, + std::string const &format) + { + mapnik::image_any image(width, height); + render(map, image, 1.0, offset_x, offset_y); + mapnik::save_to_file(image, file, format); + } -void render_tile_to_file(mapnik::Map const& map, - unsigned offset_x, unsigned offset_y, - unsigned width, unsigned height, - std::string const& file, - std::string const& format) -{ - mapnik::image_any image(width,height); - render(map,image,1.0,offset_x, offset_y); - mapnik::save_to_file(image,file,format); -} - -void render_to_file1(mapnik::Map const& map, - std::string const& filename, - std::string const& format) -{ - if (format == "svg-ng") + void render_to_file1(mapnik::Map const &map, + std::string const &filename, + std::string const &format) { -#if defined(SVG_RENDERER) - std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary); - if (!file) + if (format == "svg-ng") { - throw mapnik::image_writer_exception("could not open file for writing: " + filename); - } - using iter_type = std::ostream_iterator; - iter_type output_stream_iterator(file); - mapnik::svg_renderer ren(map,output_stream_iterator); - ren.apply(); +#if defined(SVG_RENDERER) + std::ofstream file(filename.c_str(), std::ios::out | std::ios::trunc | std::ios::binary); + if (!file) + { + throw mapnik::image_writer_exception("could not open file for writing: " + filename); + } + using iter_type = std::ostream_iterator; + iter_type output_stream_iterator(file); + mapnik::svg_renderer ren(map, output_stream_iterator); + ren.apply(); #else - throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format); + throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format); #endif - } - else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24") - { + } + else if (format == "pdf" || format == "svg" || format == "ps" || format == "ARGB32" || format == "RGB24") + { #if defined(HAVE_CAIRO) - mapnik::save_to_cairo_file(map,filename,format,1.0); + mapnik::save_to_cairo_file(map, filename, format, 1.0); #else - throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); + throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); #endif + } + else + { + mapnik::image_any image(map.width(), map.height()); + render(map, image, 1.0, 0, 0); + mapnik::save_to_file(image, filename, format); + } } - else - { - mapnik::image_any image(map.width(),map.height()); - render(map,image,1.0,0,0); - mapnik::save_to_file(image,filename,format); - } -} -void render_to_file2(mapnik::Map const& map,std::string const& filename) -{ - std::string format = mapnik::guess_type(filename); - if (format == "pdf" || format == "svg" || format =="ps") + void render_to_file2(mapnik::Map const &map, std::string const &filename) { + std::string format = mapnik::guess_type(filename); + if (format == "pdf" || format == "svg" || format == "ps") + { #if defined(HAVE_CAIRO) - mapnik::save_to_cairo_file(map,filename,format,1.0); + mapnik::save_to_cairo_file(map, filename, format, 1.0); #else - throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); + throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); #endif + } + else + { + mapnik::image_any image(map.width(), map.height()); + render(map, image, 1.0, 0, 0); + mapnik::save_to_file(image, filename); + } } - else - { - mapnik::image_any image(map.width(),map.height()); - render(map,image,1.0,0,0); - mapnik::save_to_file(image,filename); - } -} -void render_to_file3(mapnik::Map const& map, - std::string const& filename, - std::string const& format, - double scale_factor = 1.0) -{ - if (format == "svg-ng") + void render_to_file3(mapnik::Map const &map, + std::string const &filename, + std::string const &format, + double scale_factor = 1.0) { -#if defined(SVG_RENDERER) - std::ofstream file (filename.c_str(), std::ios::out|std::ios::trunc|std::ios::binary); - if (!file) + if (format == "svg-ng") { - throw mapnik::image_writer_exception("could not open file for writing: " + filename); - } - using iter_type = std::ostream_iterator; - iter_type output_stream_iterator(file); - mapnik::svg_renderer ren(map,output_stream_iterator,scale_factor); - ren.apply(); +#if defined(SVG_RENDERER) + std::ofstream file(filename.c_str(), std::ios::out | std::ios::trunc | std::ios::binary); + if (!file) + { + throw mapnik::image_writer_exception("could not open file for writing: " + filename); + } + using iter_type = std::ostream_iterator; + iter_type output_stream_iterator(file); + mapnik::svg_renderer ren(map, output_stream_iterator, scale_factor); + ren.apply(); #else - throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format); + throw mapnik::image_writer_exception("SVG backend not available, cannot write to format: " + format); #endif - } - else if (format == "pdf" || format == "svg" || format =="ps" || format == "ARGB32" || format == "RGB24") - { + } + else if (format == "pdf" || format == "svg" || format == "ps" || format == "ARGB32" || format == "RGB24") + { #if defined(HAVE_CAIRO) - mapnik::save_to_cairo_file(map,filename,format,scale_factor); + mapnik::save_to_cairo_file(map, filename, format, scale_factor); #else - throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); + throw mapnik::image_writer_exception("Cairo backend not available, cannot write to format: " + format); #endif + } + else + { + mapnik::image_any image(map.width(), map.height()); + render(map, image, scale_factor, 0, 0); + mapnik::save_to_file(image, filename, format); + } } - else + + double scale_denominator(mapnik::Map const &map, bool geographic) { - mapnik::image_any image(map.width(),map.height()); - render(map,image,scale_factor,0,0); - mapnik::save_to_file(image,filename,format); + return mapnik::scale_denominator(map.scale(), geographic); } -} -double scale_denominator(mapnik::Map const& map, bool geographic) -{ - return mapnik::scale_denominator(map.scale(), geographic); -} - -// http://docs.python.org/c-api/exceptions.html#standard-exceptions -void value_error_translator(mapnik::value_error const & ex) -{ - PyErr_SetString(PyExc_ValueError, ex.what()); -} + // http://docs.python.org/c-api/exceptions.html#standard-exceptions + void value_error_translator(mapnik::value_error const &ex) + { + PyErr_SetString(PyExc_ValueError, ex.what()); + } -void runtime_error_translator(std::runtime_error const & ex) -{ - PyErr_SetString(PyExc_RuntimeError, ex.what()); -} + void runtime_error_translator(std::runtime_error const &ex) + { + PyErr_SetString(PyExc_RuntimeError, ex.what()); + } -void out_of_range_error_translator(std::out_of_range const & ex) -{ - PyErr_SetString(PyExc_IndexError, ex.what()); -} + void out_of_range_error_translator(std::out_of_range const &ex) + { + PyErr_SetString(PyExc_IndexError, ex.what()); + } -void standard_error_translator(std::exception const & ex) -{ - PyErr_SetString(PyExc_RuntimeError, ex.what()); -} + void standard_error_translator(std::exception const &ex) + { + PyErr_SetString(PyExc_RuntimeError, ex.what()); + } -// indicator for pycairo support in the python bindings -bool has_pycairo() -{ + // indicator for pycairo support in the python bindings + bool has_pycairo() + { #if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO) #if PY_MAJOR_VERSION >= 3 - Pycairo_CAPI = (Pycairo_CAPI_t*) PyCapsule_Import(const_cast("cairo.CAPI"), 0); + Pycairo_CAPI = (Pycairo_CAPI_t *)PyCapsule_Import(const_cast("cairo.CAPI"), 0); #else - Pycairo_CAPI = (Pycairo_CAPI_t*) PyCObject_Import(const_cast("cairo"), const_cast("CAPI")); + Pycairo_CAPI = (Pycairo_CAPI_t *)PyCObject_Import(const_cast("cairo"), const_cast("CAPI")); #endif - if (Pycairo_CAPI == nullptr){ - /* - Case where pycairo support has been compiled into - mapnik but at runtime the cairo python module - is unable to be imported and therefore Pycairo surfaces - and contexts cannot be passed to mapnik.render() - */ - return false; - } - return true; + if (Pycairo_CAPI == nullptr) + { + /* + Case where pycairo support has been compiled into + mapnik but at runtime the cairo python module + is unable to be imported and therefore Pycairo surfaces + and contexts cannot be passed to mapnik.render() + */ + return false; + } + return true; #else - return false; + return false; #endif -} - + } -unsigned mapnik_version() -{ - return MAPNIK_VERSION; -} + unsigned mapnik_version() + { + return MAPNIK_VERSION; + } -std::string mapnik_version_string() -{ - return MAPNIK_VERSION_STRING; -} + std::string mapnik_version_string() + { + return MAPNIK_VERSION_STRING; + } -bool has_proj() -{ + bool has_proj() + { #if defined(MAPNIK_USE_PROJ) - return true; + return true; #else - return false; + return false; #endif -} + } -bool has_svg_renderer() -{ + bool has_svg_renderer() + { #if defined(SVG_RENDERER) - return true; + return true; #else - return false; + return false; #endif -} + } -bool has_grid_renderer() -{ + bool has_grid_renderer() + { #if defined(GRID_RENDERER) - return true; + return true; #else - return false; + return false; #endif -} + } -constexpr bool has_jpeg() -{ + constexpr bool has_jpeg() + { #if defined(HAVE_JPEG) - return true; + return true; #else - return false; + return false; #endif -} + } -constexpr bool has_png() -{ + constexpr bool has_png() + { #if defined(HAVE_PNG) - return true; + return true; #else - return false; + return false; #endif -} + } -bool has_tiff() -{ + bool has_tiff() + { #if defined(HAVE_TIFF) - return true; + return true; #else - return false; + return false; #endif -} + } -bool has_webp() -{ + bool has_webp() + { #if defined(HAVE_WEBP) - return true; + return true; #else - return false; - #endif -} + return false; +#endif + } -// indicator for cairo rendering support inside libmapnik -bool has_cairo() -{ + // indicator for cairo rendering support inside libmapnik + bool has_cairo() + { #if defined(HAVE_CAIRO) - return true; + return true; #else - return false; + return false; #endif -} + } } // namespace - -void export_color(py::module const&); -void export_composite_modes(py::module const&); -void export_coord(py::module const&); -void export_envelope(py::module const&); -void export_gamma_method(py::module const&); -void export_geometry(py::module const&); -void export_feature(py::module const&); -void export_featureset(py::module const&); -void export_font_engine(py::module const&); -void export_fontset(py::module const&); -void export_expression(py::module const&); -void export_datasource(py::module&); // non-const because of m.def(..) -void export_datasource_cache(py::module const&); +void export_color(py::module const &); +void export_composite_modes(py::module const &); +void export_coord(py::module const &); +void export_envelope(py::module const &); +void export_gamma_method(py::module const &); +void export_geometry(py::module const &); +void export_feature(py::module const &); +void export_featureset(py::module const &); +void export_font_engine(py::module const &); +void export_fontset(py::module const &); +void export_expression(py::module const &); +void export_datasource(py::module &); // non-const because of m.def(..) +void export_datasource_cache(py::module const &); #if defined(GRID_RENDERER) -void export_grid(py::module const&); -void export_grid_view(py::module const&); +void export_grid(py::module const &); +void export_grid_view(py::module const &); #endif -void export_image(py::module const&); -void export_image_view(py::module const&); -void export_layer(py::module const&); -void export_map(py::module const&); -void export_projection(py::module&); // non-const because of m.def(..) -void export_proj_transform(py::module const&); -void export_query(py::module const&); -void export_rule(py::module const&); -void export_symbolizer(py::module const&); -void export_polygon_symbolizer(py::module const&); -void export_line_symbolizer(py::module const&); -void export_point_symbolizer(py::module const&); -void export_style(py::module const&); -void export_logger(py::module const&); -void export_placement_finder(py::module const&); -void export_text_symbolizer(py::module const&); -void export_debug_symbolizer(py::module const&); -void export_markers_symbolizer(py::module const&); -void export_polygon_pattern_symbolizer(py::module const&); -void export_line_pattern_symbolizer(py::module const&); -void export_raster_symbolizer(py::module const&); -void export_palette(py::module const&); -void export_parameters(py::module const&); -void export_raster_colorizer(py::module const&); -void export_scaling_method(py::module const&); -void export_label_collision_detector(py::module const& m); -void export_dot_symbolizer(py::module const&); -void export_shield_symbolizer(py::module const&); -void export_group_symbolizer(py::module const&); -void export_building_symbolizer(py::module const&); +void export_image(py::module const &); +void export_image_view(py::module const &); +void export_layer(py::module const &); +void export_map(py::module const &); +void export_projection(py::module &); // non-const because of m.def(..) +void export_proj_transform(py::module const &); +void export_query(py::module const &); +void export_rule(py::module const &); +void export_symbolizer(py::module const &); +void export_polygon_symbolizer(py::module const &); +void export_line_symbolizer(py::module const &); +void export_point_symbolizer(py::module const &); +void export_style(py::module const &); +void export_logger(py::module const &); +void export_placement_finder(py::module const &); +void export_text_symbolizer(py::module const &); +void export_debug_symbolizer(py::module const &); +void export_markers_symbolizer(py::module const &); +void export_polygon_pattern_symbolizer(py::module const &); +void export_line_pattern_symbolizer(py::module const &); +void export_raster_symbolizer(py::module const &); +void export_palette(py::module const &); +void export_parameters(py::module const &); +void export_raster_colorizer(py::module const &); +void export_scaling_method(py::module const &); +void export_label_collision_detector(py::module const &m); +void export_dot_symbolizer(py::module const &); +void export_shield_symbolizer(py::module const &); +void export_group_symbolizer(py::module const &); +void export_building_symbolizer(py::module const &); using mapnik::load_map; using mapnik::load_map_string; using mapnik::save_map; using mapnik::save_map_to_string; - -PYBIND11_MODULE(_mapnik, m) { - export_color(m); - export_composite_modes(m); - export_coord(m); - export_envelope(m); - export_geometry(m); - export_gamma_method(m); - export_feature(m); - export_featureset(m); - export_font_engine(m); - export_fontset(m); - export_expression(m); - export_datasource(m); - export_datasource_cache(m); +PYBIND11_MODULE(_mapnik, m) +{ + // Work around occasional CPython failures parsing generated __text_signature__ + // strings (seen as: "Internal error while parsing type signature"). + // This only affects Python-side introspection; it does not change behavior. + py::options options; + options.disable_function_signatures(); + + safe_export("color", [&]() + { export_color(m); }); + safe_export("composite_modes", [&]() + { export_composite_modes(m); }); + safe_export("coord", [&]() + { export_coord(m); }); + safe_export("envelope", [&]() + { export_envelope(m); }); + safe_export("geometry", [&]() + { export_geometry(m); }); + safe_export("gamma_method", [&]() + { export_gamma_method(m); }); + safe_export("feature", [&]() + { export_feature(m); }); + safe_export("featureset", [&]() + { export_featureset(m); }); + safe_export("font_engine", [&]() + { export_font_engine(m); }); + safe_export("fontset", [&]() + { export_fontset(m); }); + safe_export("expression", [&]() + { export_expression(m); }); + safe_export("datasource", [&]() + { export_datasource(m); }); + safe_export("datasource_cache", [&]() + { export_datasource_cache(m); }); #if defined(GRID_RENDERER) - export_grid(m); - export_grid_view(m); + safe_export("grid", [&]() + { export_grid(m); }); + safe_export("grid_view", [&]() + { export_grid_view(m); }); #endif - export_image(m); - export_image_view(m); - export_layer(m); - export_map(m); - export_projection(m); - export_proj_transform(m); - export_query(m); - export_rule(m); - export_symbolizer(m); - export_polygon_symbolizer(m); - export_line_symbolizer(m); - export_point_symbolizer(m); - export_style(m); - export_logger(m); - export_placement_finder(m); - export_text_symbolizer(m); - export_palette(m); - export_parameters(m); - export_debug_symbolizer(m); - export_markers_symbolizer(m); - export_polygon_pattern_symbolizer(m); - export_line_pattern_symbolizer(m); - export_raster_symbolizer(m); - export_raster_colorizer(m); - export_scaling_method(m); - export_label_collision_detector(m); - export_dot_symbolizer(m); - export_shield_symbolizer(m); - export_group_symbolizer(m); - export_building_symbolizer(m); + safe_export("image", [&]() + { export_image(m); }); + safe_export("image_view", [&]() + { export_image_view(m); }); + safe_export("layer", [&]() + { export_layer(m); }); + safe_export("map", [&]() + { export_map(m); }); + safe_export("projection", [&]() + { export_projection(m); }); + safe_export("proj_transform", [&]() + { export_proj_transform(m); }); + safe_export("query", [&]() + { export_query(m); }); + safe_export("rule", [&]() + { export_rule(m); }); + safe_export("symbolizer", [&]() + { export_symbolizer(m); }); + safe_export("polygon_symbolizer", [&]() + { export_polygon_symbolizer(m); }); + safe_export("line_symbolizer", [&]() + { export_line_symbolizer(m); }); + safe_export("point_symbolizer", [&]() + { export_point_symbolizer(m); }); + safe_export("style", [&]() + { export_style(m); }); + safe_export("logger", [&]() + { export_logger(m); }); + safe_export("placement_finder", [&]() + { export_placement_finder(m); }); + safe_export("text_symbolizer", [&]() + { export_text_symbolizer(m); }); + safe_export("palette", [&]() + { export_palette(m); }); + safe_export("parameters", [&]() + { export_parameters(m); }); + safe_export("debug_symbolizer", [&]() + { export_debug_symbolizer(m); }); + safe_export("markers_symbolizer", [&]() + { export_markers_symbolizer(m); }); + safe_export("polygon_pattern_symbolizer", [&]() + { export_polygon_pattern_symbolizer(m); }); + safe_export("line_pattern_symbolizer", [&]() + { export_line_pattern_symbolizer(m); }); + safe_export("raster_symbolizer", [&]() + { export_raster_symbolizer(m); }); + safe_export("raster_colorizer", [&]() + { export_raster_colorizer(m); }); + safe_export("scaling_method", [&]() + { export_scaling_method(m); }); + safe_export("label_collision_detector", [&]() + { export_label_collision_detector(m); }); + safe_export("dot_symbolizer", [&]() + { export_dot_symbolizer(m); }); + safe_export("shield_symbolizer", [&]() + { export_shield_symbolizer(m); }); + safe_export("group_symbolizer", [&]() + { export_group_symbolizer(m); }); + safe_export("building_symbolizer", [&]() + { export_building_symbolizer(m); }); // - m.def("version", &mapnik_version,"Get the Mapnik version number"); - m.def("version_string", &mapnik_version_string,"Get the Mapnik version string"); + m.def("version", &mapnik_version, "Get the Mapnik version number"); + m.def("version_string", &mapnik_version_string, "Get the Mapnik version string"); m.def("has_proj", &has_proj, "Get proj status"); m.def("has_jpeg", &has_jpeg, "Get jpeg read/write support status"); m.def("has_png", &has_png, "Get png read/write support status"); @@ -691,14 +762,14 @@ PYBIND11_MODULE(_mapnik, m) { m.def("load_map", &load_map, py::arg("Map"), py::arg("filename"), - py::arg("strict")=false, - py::arg("base_path") = "" ); + py::arg("strict") = false, + py::arg("base_path") = ""); m.def("load_map_from_string", &load_map_string, py::arg("Map"), py::arg("str"), - py::arg("strict")=false, - py::arg("base_path") = "" ); + py::arg("strict") = false, + py::arg("base_path") = ""); // render m.def("render", &render, @@ -717,139 +788,130 @@ PYBIND11_MODULE(_mapnik, m) { py::arg("offset_y") = 0); #if defined(HAVE_CAIRO) && defined(HAVE_PYCAIRO) - m.def("render",&render3, - "\n" - "Render Map to Cairo Surface using offsets\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, render, load_map\n" - ">>> from cairo import SVGSurface\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> render(m,surface,1,1)\n" - "\n" - ); - - m.def("render",&render4, - "\n" - "Render Map to Cairo Surface\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, render, load_map\n" - ">>> from cairo import SVGSurface\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> render(m,surface)\n" - "\n" - ); - - m.def("render",&render5, - "\n" - "Render Map to Cairo Context using offsets\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, render, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> ctx = Context(surface)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> render(m,context,1,1)\n" - "\n" - ); - - m.def("render",&render6, - "\n" - "Render Map to Cairo Context\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, render, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> ctx = Context(surface)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> render(m,context)\n" - "\n" - ); + m.def("render", &render3, + "\n" + "Render Map to Cairo Surface using offsets\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, render, load_map\n" + ">>> from cairo import SVGSurface\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> render(m,surface,1,1)\n" + "\n"); + + m.def("render", &render4, + "\n" + "Render Map to Cairo Surface\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, render, load_map\n" + ">>> from cairo import SVGSurface\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> render(m,surface)\n" + "\n"); + + m.def("render", &render5, + "\n" + "Render Map to Cairo Context using offsets\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, render, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> ctx = Context(surface)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> render(m,context,1,1)\n" + "\n"); + + m.def("render", &render6, + "\n" + "Render Map to Cairo Context\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, render, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> ctx = Context(surface)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> render(m,context)\n" + "\n"); m.def("render_with_detector", &render_with_detector2, - "\n" - "Render Map to Cairo Context using a pre-constructed detector.\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> ctx = Context(surface)\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> detector = LabelCollisionDetector(m)\n" - ">>> render_with_detector(m, ctx, detector)\n" - ); + "\n" + "Render Map to Cairo Context using a pre-constructed detector.\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> ctx = Context(surface)\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> detector = LabelCollisionDetector(m)\n" + ">>> render_with_detector(m, ctx, detector)\n"); m.def("render_with_detector", &render_with_detector3, - "\n" - "Render Map to Cairo Context using a pre-constructed detector, scale and offsets.\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> ctx = Context(surface)\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> detector = LabelCollisionDetector(m)\n" - ">>> render_with_detector(m, ctx, detector, 1, 1, 1)\n" - ); + "\n" + "Render Map to Cairo Context using a pre-constructed detector, scale and offsets.\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> ctx = Context(surface)\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> detector = LabelCollisionDetector(m)\n" + ">>> render_with_detector(m, ctx, detector, 1, 1, 1)\n"); m.def("render_with_detector", &render_with_detector4, - "\n" - "Render Map to Cairo Surface using a pre-constructed detector.\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> detector = LabelCollisionDetector(m)\n" - ">>> render_with_detector(m, surface, detector)\n" - ); + "\n" + "Render Map to Cairo Surface using a pre-constructed detector.\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> detector = LabelCollisionDetector(m)\n" + ">>> render_with_detector(m, surface, detector)\n"); m.def("render_with_detector", &render_with_detector5, - "\n" - "Render Map to Cairo Surface using a pre-constructed detector, scale and offsets.\n" - "\n" - "Usage:\n" - ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" - ">>> from cairo import SVGSurface, Context\n" - ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" - ">>> m = Map(256,256)\n" - ">>> load_map(m,'mapfile.xml')\n" - ">>> detector = LabelCollisionDetector(m)\n" - ">>> render_with_detector(m, surface, detector, 1, 1, 1)\n" - ); + "\n" + "Render Map to Cairo Surface using a pre-constructed detector, scale and offsets.\n" + "\n" + "Usage:\n" + ">>> from mapnik import Map, LabelCollisionDetector, render_with_detector, load_map\n" + ">>> from cairo import SVGSurface, Context\n" + ">>> surface = SVGSurface('image.svg', m.width, m.height)\n" + ">>> m = Map(256,256)\n" + ">>> load_map(m,'mapfile.xml')\n" + ">>> detector = LabelCollisionDetector(m)\n" + ">>> render_with_detector(m, surface, detector, 1, 1, 1)\n"); #endif m.def("render_layer", &render_layer2, py::arg("map"), py::arg("image"), py::arg("layer"), - py::arg("scale_factor")=1.0, - py::arg("offset_x")=0, - py::arg("offset_y")=0 - ); + py::arg("scale_factor") = 1.0, + py::arg("offset_x") = 0, + py::arg("offset_y") = 0); #if defined(GRID_RENDERER) m.def("render_layer", &mapnik::render_layer_for_grid, - py::arg("map"), - py::arg("grid"), - py::arg("layer"), - py::arg("fields") = py::list(), - py::arg("scale_factor")=1.0, - py::arg("offset_x")=0, - py::arg("offset_y")=0); + py::arg("map"), + py::arg("grid"), + py::arg("layer"), + py::arg("fields") = py::list(), + py::arg("scale_factor") = 1.0, + py::arg("offset_x") = 0, + py::arg("offset_y") = 0); #endif // save @@ -870,8 +932,7 @@ PYBIND11_MODULE(_mapnik, m) { ">>> from mapnik import clear_cache\n" ">>> clear_cache()\n"); - - m.def("render_to_file",&render_to_file1, + m.def("render_to_file", &render_to_file1, "\n" "Render Map to file using explicit image type.\n" "\n" @@ -889,7 +950,7 @@ PYBIND11_MODULE(_mapnik, m) { ">>> render_to_file(m,'top_quality.jpeg','jpeg100')\n" ">>> render_to_file(m,'medium_quality.jpeg','jpeg50')\n"); - m.def("render_to_file",&render_to_file2, + m.def("render_to_file", &render_to_file2, "\n" "Render Map to file (type taken from file extension)\n" "\n" @@ -899,7 +960,7 @@ PYBIND11_MODULE(_mapnik, m) { ">>> render_to_file(m,'image.jpeg')\n" "\n"); - m.def("render_to_file",&render_to_file3, + m.def("render_to_file", &render_to_file3, "\n" "Render Map to file using explicit image type and scale factor.\n" "\n" @@ -982,9 +1043,9 @@ PYBIND11_MODULE(_mapnik, m) { // #if defined(GRID_RENDERER) // #include "python_grid_utils.hpp" // #endif -//#include "mapnik_value_converter.hpp" +// #include "mapnik_value_converter.hpp" // #include "mapnik_enumeration_wrapper_converter.hpp" -//#include "mapnik_threads.hpp" +// #include "mapnik_threads.hpp" // #include "python_optional.hpp" // #include // #if defined(SHAPE_MEMORY_MAPPED_FILE) @@ -1056,8 +1117,6 @@ PYBIND11_MODULE(_mapnik, m) { // } // #endif - - // #pragma GCC diagnostic push // #pragma GCC diagnostic ignored "-Wunused-local-typedef" // BOOST_PYTHON_FUNCTION_OVERLOADS(load_map_overloads, load_map, 2, 4) @@ -1133,7 +1192,6 @@ PYBIND11_MODULE(_mapnik, m) { // export_label_collision_detector(); // export_logger(); - // def("render_tile_to_file",&render_tile_to_file, // "\n" // "TODO\n" diff --git a/src/mapnik_value_converter.hpp b/src/mapnik_value_converter.hpp index fdb6be860..2a785aaf3 100644 --- a/src/mapnik_value_converter.hpp +++ b/src/mapnik_value_converter.hpp @@ -36,7 +36,7 @@ struct value_converter { PyObject * operator() (mapnik::value_integer val) const { - return ::PyLong_FromLongLong(val); + return ::PyLong_FromLong(val); } PyObject * operator() (mapnik::value_double val) const @@ -126,7 +126,7 @@ struct type_caster { PyObject *tmp = PyNumber_Long(source); if (!tmp) return false; - value = PyLong_AsLongLong(tmp); + value = PyLong_AsLong(tmp); Py_DecRef(tmp); return !PyErr_Occurred(); } @@ -180,7 +180,7 @@ struct type_caster { PyObject *tmp = PyNumber_Long(source); if (!tmp) return false; - value = PyLong_AsLongLong(tmp); + value = PyLong_AsLong(tmp); Py_DecRef(tmp); return !PyErr_Occurred(); } diff --git a/src/python_grid_utils.cpp b/src/python_grid_utils.cpp index 9333f6076..7decf4e79 100644 --- a/src/python_grid_utils.cpp +++ b/src/python_grid_utils.cpp @@ -33,296 +33,303 @@ #include #include #include "python_grid_utils.hpp" +#include "mapnik_value_converter.hpp" // stl #include #include -namespace mapnik { - - - -template -void grid2utf(T const& grid_type, - py::list& l, - std::vector& key_order) +namespace mapnik { - using code_point_t = std::uint32_t; - using keys_type = std::map< typename T::lookup_type, typename T::value_type>; - using keys_iterator = typename keys_type::iterator; - typename T::data_type const& data = grid_type.data(); - typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys(); - typename T::feature_key_type::const_iterator feature_pos; + template + void grid2utf(T const &grid_type, + py::list &l, + std::vector &key_order) + { + using code_point_t = std::uint32_t; + using keys_type = std::map; + using keys_iterator = typename keys_type::iterator; - keys_type keys; - // start counting at utf8 codepoint 32, aka space character - std::uint16_t codepoint = 32; + typename T::data_type const &data = grid_type.data(); + typename T::feature_key_type const &feature_keys = grid_type.get_feature_keys(); + typename T::feature_key_type::const_iterator feature_pos; - std::size_t array_size = data.width(); - for (std::size_t y = 0; y < data.height(); ++y) - { - std::uint16_t idx = 0; - const std::unique_ptr line(new code_point_t[array_size]); - typename T::value_type const* row = data.get_row(y); - for (std::size_t x = 0; x < data.width(); ++x) + keys_type keys; + // start counting at utf8 codepoint 32, aka space character + std::uint16_t codepoint = 32; + + std::size_t array_size = data.width(); + for (std::size_t y = 0; y < data.height(); ++y) { - typename T::value_type feature_id = row[x]; - feature_pos = feature_keys.find(feature_id); - if (feature_pos != feature_keys.end()) + std::uint16_t idx = 0; + const std::unique_ptr line(new code_point_t[array_size]); + typename T::value_type const *row = data.get_row(y); + for (std::size_t x = 0; x < data.width(); ++x) { - mapnik::grid::lookup_type val = feature_pos->second; - keys_iterator key_pos = keys.find(val); - if (key_pos == keys.end()) + typename T::value_type feature_id = row[x]; + feature_pos = feature_keys.find(feature_id); + if (feature_pos != feature_keys.end()) { - // Create a new entry for this key. Skip the codepoints that - // can't be encoded directly in JSON. - if (codepoint == 34) ++codepoint; // Skip " - else if (codepoint == 92) ++codepoint; // Skip backslash - if (feature_id == mapnik::grid::base_mask) + mapnik::grid::lookup_type val = feature_pos->second; + keys_iterator key_pos = keys.find(val); + if (key_pos == keys.end()) { - keys[""] = codepoint; - key_order.push_back(""); + // Create a new entry for this key. Skip the codepoints that + // can't be encoded directly in JSON. + if (codepoint == 34) + ++codepoint; // Skip " + else if (codepoint == 92) + ++codepoint; // Skip backslash + if (feature_id == mapnik::grid::base_mask) + { + keys[""] = codepoint; + key_order.push_back(""); + } + else + { + keys[val] = codepoint; + key_order.push_back(val); + } + line[idx++] = static_cast(codepoint); + ++codepoint; } else { - keys[val] = codepoint; - key_order.push_back(val); + line[idx++] = static_cast(key_pos->second); } - line[idx++] = static_cast(codepoint); - ++codepoint; - } - else - { - line[idx++] = static_cast(key_pos->second); } + // else, shouldn't get here... } - // else, shouldn't get here... + l.append(PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, line.get(), array_size)); } - l.append(PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, line.get(), array_size)); } -} + template + void grid2utf(T const &grid_type, + py::list &l, + std::vector &key_order, + unsigned int resolution) + { + using code_point_t = std::uint32_t; + using keys_type = std::map; + using keys_iterator = typename keys_type::iterator; -template -void grid2utf(T const& grid_type, - py::list& l, - std::vector& key_order, - unsigned int resolution) -{ - using code_point_t = std::uint32_t; - using keys_type = std::map< typename T::lookup_type, typename T::value_type>; - using keys_iterator = typename keys_type::iterator; - - typename T::feature_key_type const& feature_keys = grid_type.get_feature_keys(); - typename T::feature_key_type::const_iterator feature_pos; + typename T::feature_key_type const &feature_keys = grid_type.get_feature_keys(); + typename T::feature_key_type::const_iterator feature_pos; - keys_type keys; - // start counting at utf8 codepoint 32, aka space character - std::uint16_t codepoint = 32; + keys_type keys; + // start counting at utf8 codepoint 32, aka space character + std::uint16_t codepoint = 32; - unsigned array_size = std::ceil(grid_type.width()/static_cast(resolution)); - for (unsigned y = 0; y < grid_type.height(); y=y+resolution) - { - std::uint16_t idx = 0; - const std::unique_ptr line(new code_point_t[array_size]); - mapnik::grid::value_type const* row = grid_type.get_row(y); - for (unsigned x = 0; x < grid_type.width(); x=x+resolution) + unsigned array_size = std::ceil(grid_type.width() / static_cast(resolution)); + for (unsigned y = 0; y < grid_type.height(); y = y + resolution) { - typename T::value_type feature_id = row[x]; - feature_pos = feature_keys.find(feature_id); - if (feature_pos != feature_keys.end()) + std::uint16_t idx = 0; + const std::unique_ptr line(new code_point_t[array_size]); + mapnik::grid::value_type const *row = grid_type.get_row(y); + for (unsigned x = 0; x < grid_type.width(); x = x + resolution) { - mapnik::grid::lookup_type val = feature_pos->second; - keys_iterator key_pos = keys.find(val); - if (key_pos == keys.end()) + typename T::value_type feature_id = row[x]; + feature_pos = feature_keys.find(feature_id); + if (feature_pos != feature_keys.end()) { - // Create a new entry for this key. Skip the codepoints that - // can't be encoded directly in JSON. - if (codepoint == 34) ++codepoint; // Skip " - else if (codepoint == 92) ++codepoint; // Skip backslash - if (feature_id == mapnik::grid::base_mask) + mapnik::grid::lookup_type val = feature_pos->second; + keys_iterator key_pos = keys.find(val); + if (key_pos == keys.end()) { - keys[""] = codepoint; - key_order.push_back(""); + // Create a new entry for this key. Skip the codepoints that + // can't be encoded directly in JSON. + if (codepoint == 34) + ++codepoint; // Skip " + else if (codepoint == 92) + ++codepoint; // Skip backslash + if (feature_id == mapnik::grid::base_mask) + { + keys[""] = codepoint; + key_order.push_back(""); + } + else + { + keys[val] = codepoint; + key_order.push_back(val); + } + line[idx++] = static_cast(codepoint); + ++codepoint; } else { - keys[val] = codepoint; - key_order.push_back(val); + line[idx++] = static_cast(key_pos->second); } - line[idx++] = static_cast(codepoint); - ++codepoint; - } - else - { - line[idx++] = static_cast(key_pos->second); } + // else, shouldn't get here... } - // else, shouldn't get here... + l.append(PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, line.get(), array_size)); } - l.append(PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, line.get(), array_size)); } -} -template -void write_features(T const& grid_type, - py::dict& feature_data, - std::vector const& key_order) -{ - typename T::feature_type const& g_features = grid_type.get_grid_features(); - if (g_features.size() <= 0) + template + void write_features(T const &grid_type, + py::dict &feature_data, + std::vector const &key_order) { - return; - } - - std::set const& attributes = grid_type.get_fields(); - typename T::feature_type::const_iterator feat_end = g_features.end(); - for ( std::string const& key_item :key_order ) - { - if (key_item.empty()) + typename T::feature_type const &g_features = grid_type.get_grid_features(); + if (g_features.size() <= 0) { - continue; + return; } - typename T::feature_type::const_iterator feat_itr = g_features.find(key_item); - if (feat_itr == feat_end) + std::set const &attributes = grid_type.get_fields(); + typename T::feature_type::const_iterator feat_end = g_features.end(); + for (std::string const &key_item : key_order) { - continue; - } + if (key_item.empty()) + { + continue; + } - bool found = false; - py::dict feat; - mapnik::feature_ptr feature = feat_itr->second; - for ( std::string const& attr : attributes ) - { - if (attr == "__id__") + typename T::feature_type::const_iterator feat_itr = g_features.find(key_item); + if (feat_itr == feat_end) { - feat[attr.c_str()] = feature->id(); + continue; } - else if (feature->has_key(attr)) + + bool found = false; + py::dict feat; + mapnik::feature_ptr feature = feat_itr->second; + for (std::string const &attr : attributes) { - found = true; - feat[attr.c_str()] = feature->get(attr); + if (attr == "__id__") + { + feat[attr.c_str()] = feature->id(); + } + else if (feature->has_key(attr)) + { + found = true; + feat[attr.c_str()] = feature->get(attr); + } } - } - if (found) - { - feature_data[feat_itr->first.c_str()] = feat; + if (found) + { + feature_data[feat_itr->first.c_str()] = feat; + } } } -} - -template -void grid_encode_utf(T const& grid_type, - py::dict & json, - bool add_features, - unsigned int resolution) -{ - // convert buffer to utf and gather key order - py::list l; - std::vector key_order; - - if (resolution != 1) - { - mapnik::grid2utf(grid_type,l,key_order,resolution); - } - else - { - mapnik::grid2utf(grid_type,l,key_order); - } - // convert key order to proper python list - py::list keys_a; - for ( typename T::lookup_type const& key_id : key_order ) + template + void grid_encode_utf(T const &grid_type, + py::dict &json, + bool add_features, + unsigned int resolution) { - keys_a.append(key_id); - } - - // gather feature data - py::dict feature_data; - if (add_features) { - mapnik::write_features(grid_type,feature_data,key_order); - } + // convert buffer to utf and gather key order + py::list l; + std::vector key_order; - json["grid"] = l; - json["keys"] = keys_a; - json["data"] = feature_data; - -} + if (resolution != 1) + { + mapnik::grid2utf(grid_type, l, key_order, resolution); + } + else + { + mapnik::grid2utf(grid_type, l, key_order); + } -template -py::dict grid_encode( T const& grid, std::string const& format, bool add_features, unsigned int resolution) -{ - if (format == "utf") { - py::dict json; - grid_encode_utf(grid,json,add_features,resolution); - return json; - } - else - { - std::stringstream s; - s << "'utf' is currently the only supported encoding format."; - throw mapnik::value_error(s.str()); - } -} + // convert key order to proper python list + py::list keys_a; + for (typename T::lookup_type const &key_id : key_order) + { + // Explicitly convert mapnik::value to Python object to avoid type registration issues + keys_a.append(py::cast(key_id)); + } -template py::dict grid_encode( mapnik::grid const& grid, std::string const& format, bool add_features, unsigned int resolution); -template py::dict grid_encode( mapnik::grid_view const& grid, std::string const& format, bool add_features, unsigned int resolution); + // gather feature data + py::dict feature_data; + if (add_features) + { + mapnik::write_features(grid_type, feature_data, key_order); + } -void render_layer_for_grid(mapnik::Map const& map, - mapnik::grid & grid, - unsigned layer_idx, - py::list const& fields, - double scale_factor, - unsigned offset_x, - unsigned offset_y) -{ - std::vector const& layers = map.layers(); - std::size_t layer_num = layers.size(); - if (layer_idx >= layer_num) { - std::ostringstream s; - s << "Zero-based layer index '" << layer_idx << "' not valid, only '" - << layer_num << "' layers are in map\n"; - throw std::runtime_error(s.str()); + json["grid"] = l; + json["keys"] = keys_a; + json["data"] = feature_data; } - // convert python list to std::set - std::size_t num_fields = py::len(fields); - for(std::size_t i = 0; i < num_fields; ++i) { - py::handle handle = fields[i]; - if (py::isinstance(handle)) + template + py::dict grid_encode(T const &grid, std::string const &format, bool add_features, unsigned int resolution) + { + if (format == "utf") { - grid.add_field(handle.cast()); + py::dict json; + grid_encode_utf(grid, json, add_features, resolution); + return json; } else { std::stringstream s; - s << "list of field names must be strings"; + s << "'utf' is currently the only supported encoding format."; throw mapnik::value_error(s.str()); } } - // copy field names - std::set attributes = grid.get_fields(); - // todo - make this a static constant - std::string known_id_key = "__id__"; - if (attributes.find(known_id_key) != attributes.end()) - { - attributes.erase(known_id_key); - } + template py::dict grid_encode(mapnik::grid const &grid, std::string const &format, bool add_features, unsigned int resolution); + template py::dict grid_encode(mapnik::grid_view const &grid, std::string const &format, bool add_features, unsigned int resolution); - std::string join_field = grid.get_key(); - if (known_id_key != join_field && - attributes.find(join_field) == attributes.end()) + void render_layer_for_grid(mapnik::Map const &map, + mapnik::grid &grid, + unsigned layer_idx, + py::list const &fields, + double scale_factor, + unsigned offset_x, + unsigned offset_y) { - attributes.insert(join_field); - } + std::vector const &layers = map.layers(); + std::size_t layer_num = layers.size(); + if (layer_idx >= layer_num) + { + std::ostringstream s; + s << "Zero-based layer index '" << layer_idx << "' not valid, only '" + << layer_num << "' layers are in map\n"; + throw std::runtime_error(s.str()); + } - mapnik::grid_renderer ren(map,grid,scale_factor,offset_x,offset_y); - mapnik::layer const& layer = layers[layer_idx]; - ren.apply(layer,attributes); -} + // convert python list to std::set + std::size_t num_fields = py::len(fields); + for (std::size_t i = 0; i < num_fields; ++i) + { + py::handle handle = fields[i]; + if (py::isinstance(handle)) + { + grid.add_field(handle.cast()); + } + else + { + std::stringstream s; + s << "list of field names must be strings"; + throw mapnik::value_error(s.str()); + } + } + + // copy field names + std::set attributes = grid.get_fields(); + // todo - make this a static constant + std::string known_id_key = "__id__"; + if (attributes.find(known_id_key) != attributes.end()) + { + attributes.erase(known_id_key); + } + + std::string join_field = grid.get_key(); + if (known_id_key != join_field && + attributes.find(join_field) == attributes.end()) + { + attributes.insert(join_field); + } + + mapnik::grid_renderer ren(map, grid, scale_factor, offset_x, offset_y); + mapnik::layer const &layer = layers[layer_idx]; + ren.apply(layer, attributes); + } } // namespace mapnik diff --git a/test/python_tests/render_test.py b/test/python_tests/render_test.py index 83e9268be..895161eef 100644 --- a/test/python_tests/render_test.py +++ b/test/python_tests/render_test.py @@ -2,7 +2,7 @@ import tempfile import mapnik import pytest -from .utilities import execution_path +from .utilities import execution_path, images_almost_equal @pytest.fixture(scope="module") def setup(): @@ -254,4 +254,7 @@ def test_render_with_scale_factor(): # color png actual = mapnik.Image.open(actual_file) expected = mapnik.Image.open(expected_file) - assert actual.to_string('png32') == expected.to_string('png32'), 'failed comparing actual (%s) and expected (%s)' % (actual_file, expected_file) + # Use pixel-level comparison instead of strict byte comparison to handle + # PNG encoding differences (compression, metadata, etc.) while images look identical. + # Allow tolerance=1 for minor rendering differences (e.g., antialiasing, rounding). + images_almost_equal(actual, expected, tolerance=1) diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..c89f53747 --- /dev/null +++ b/uv.lock @@ -0,0 +1,170 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mapnik" +version = "4.2.0" +source = { editable = "." } + +[package.optional-dependencies] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0" }, +] +provides-extras = ["test"] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +]