diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index e0d0279b..6e5d8c84 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -29,14 +29,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10"] - thing-to-test: ["flake8", "4.2", "5.2"] + python-version: ["3.13"] + thing-to-test: ["flake8", "4.2", "5.2", "6"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} diff --git a/mapit/djangopatch.py b/mapit/djangopatch.py index 2cacced8..9fdf79e4 100644 --- a/mapit/djangopatch.py +++ b/mapit/djangopatch.py @@ -1,104 +1,20 @@ -# From https://gist.github.com/bpartridge/26a11b28415d706bfb9993fc28767d68 - import django -def patch_geos_signatures(): - """ - Patch GEOS to function on macOS arm64 and presumably - other odd architectures by ensuring that call signatures - are explicit, and that Django 4 bugfixes are backported. - - Should work on Django 2.2+, minimally tested, caveat emptor. - """ - import logging - - from ctypes import POINTER, c_uint, c_int - from django.contrib.gis.geos import GeometryCollection, Polygon - from django.contrib.gis.geos import prototypes as capi - from django.contrib.gis.geos.prototypes import GEOM_PTR - from django.contrib.gis.geos.prototypes.geom import GeomOutput - from django.contrib.gis.geos.libgeos import geos_version, lgeos - from django.contrib.gis.geos.linestring import LineString - - logger = logging.getLogger("geos_patch") - - _geos_version = geos_version() - logger.debug("GEOS: %s %s", _geos_version, repr(lgeos)) - - # Backport https://code.djangoproject.com/ticket/30274 - def new_linestring_iter(self): - for i in range(len(self)): - yield self[i] - - LineString.__iter__ = new_linestring_iter - - # macOS arm64 requires that we have explicit argtypes for cffi calls. - # Patch in argtypes for `create_polygon` and `create_collection`, - # and then ensure their prep functions do NOT use byref so that the - # arrays (`(GEOM_PTR * length)(...)`) auto-convert into `Geometry**`. - # create_empty_polygon doesn't need to be patched as it takes no args. - - # Geometry* - # GEOSGeom_createPolygon_r(GEOSContextHandle_t extHandle, - # Geometry* shell, Geometry** holes, unsigned int nholes) - capi.create_polygon = GeomOutput( - "GEOSGeom_createPolygon", argtypes=[GEOM_PTR, POINTER(GEOM_PTR), c_uint] - ) +def patch_set_3d(): + from django.contrib.gis.gdal import OGRGeometry - # Geometry* - # GEOSGeom_createCollection_r(GEOSContextHandle_t extHandle, - # int type, Geometry** geoms, unsigned int ngeoms) - capi.create_collection = GeomOutput( - "GEOSGeom_createCollection", argtypes=[c_int, POINTER(GEOM_PTR), c_uint] - ) - - # The below implementations are taken directly from Django 2.2.25 source; - # the only changes are unwrapping calls to byref(). - - def new_create_polygon(self, length, items): - # Instantiate LinearRing objects if necessary, but don't clone them yet - # _construct_ring will throw a TypeError if a parameter isn't a valid ring - # If we cloned the pointers here, we wouldn't be able to clean up - # in case of error. - if not length: - return capi.create_empty_polygon() - - rings = [] - for r in items: - if isinstance(r, GEOM_PTR): - rings.append(r) - else: - rings.append(self._construct_ring(r)) - - shell = self._clone(rings.pop(0)) - - n_holes = length - 1 - if n_holes: - holes = (GEOM_PTR * n_holes)(*[self._clone(r) for r in rings]) - holes_param = holes + def set_3d(self, value): + """Set if this geometry has Z coordinates.""" + if value is True: + self.coord_dim = 3 + elif value is False: + self.coord_dim = 2 else: - holes_param = None - - return capi.create_polygon(shell, holes_param, c_uint(n_holes)) - - Polygon._create_polygon = new_create_polygon - - # Need to patch to not call byref so that we can cast to a pointer - def new_create_collection(self, length, items): - # Creating the geometry pointer array. - geoms = (GEOM_PTR * length)( - *[ - # this is a little sloppy, but makes life easier - # allow GEOSGeometry types (python wrappers) or pointer types - capi.geom_clone(getattr(g, "ptr", g)) - for g in items - ] - ) - return capi.create_collection(c_int(self._typeid), geoms, c_uint(length)) + raise ValueError(f"Input to 'set_3d' must be a boolean, got '{value!r}'.") - GeometryCollection._create_collection = new_create_collection + OGRGeometry.set_3d = set_3d -if django.get_version() < '4.0.1': - patch_geos_signatures() +if django.get_version() < '5.1': + patch_set_3d() diff --git a/mapit/management/command_utils.py b/mapit/management/command_utils.py index fc005924..83f24048 100644 --- a/mapit/management/command_utils.py +++ b/mapit/management/command_utils.py @@ -56,7 +56,7 @@ def save_polygons(lookup, write_to_stdout=True): if g.point_count < 4: continue # Make sure it is two-dimensional - g.coord_dim = 2 + g.set_3d(False) m.polygons.create(polygon=g.wkb) # m.polygon = g.wkt # m.save() diff --git a/mapit/models.py b/mapit/models.py index d51d9164..08de91bb 100644 --- a/mapit/models.py +++ b/mapit/models.py @@ -18,9 +18,8 @@ def materialized(): - version = connection.cursor().connection.server_version materialized = '' - if version >= 120000: + if connection.pg_version >= 120000: materialized = 'MATERIALIZED' return materialized @@ -191,8 +190,8 @@ def intersect(self, query_type, area, types, generation): params = [area.id, area.id, generation.id, generation.id] if types: - params.append(tuple(types)) - query_area_type = ' AND mapit_area.type_id IN (SELECT id FROM mapit_type WHERE code IN %s) ' + params.append(list(types)) + query_area_type = ' AND mapit_area.type_id IN (SELECT id FROM mapit_type WHERE code = ANY(%s)) ' else: query_area_type = '' @@ -452,7 +451,7 @@ def filter_by_area(self, area, limit=''): WHERE area_id = %s''' query = ''' WITH target AS %s ( %s ) -SELECT "mapit_postcode"."id", "mapit_postcode"."postcode", "mapit_postcode"."location"::bytea +SELECT "mapit_postcode"."id", "mapit_postcode"."postcode", "mapit_postcode"."location" FROM mapit_postcode, target WHERE ST_CoveredBy(location, target.division) %s diff --git a/mapit/views/areas.py b/mapit/views/areas.py index 1791f41e..2cc6073a 100644 --- a/mapit/views/areas.py +++ b/mapit/views/areas.py @@ -1,5 +1,5 @@ import re -from psycopg2 import InternalError +from django.db import InternalError from django.db.utils import DatabaseError from django.utils.translation import gettext as _ diff --git a/mapit_gb/management/commands/mapit_UK_add_ons_to_gss.py b/mapit_gb/management/commands/mapit_UK_add_ons_to_gss.py index cee7ccb1..1d781d53 100644 --- a/mapit_gb/management/commands/mapit_UK_add_ons_to_gss.py +++ b/mapit_gb/management/commands/mapit_UK_add_ons_to_gss.py @@ -7,7 +7,7 @@ import sys from django.core.management.base import BaseCommand from mapit.models import Area, CodeType -from psycopg2 import IntegrityError +from django.db import IntegrityError python_version = sys.version_info[0] diff --git a/mapit_gb/management/commands/mapit_UK_update_ons_ids.py b/mapit_gb/management/commands/mapit_UK_update_ons_ids.py index cb2e420b..e758db9e 100644 --- a/mapit_gb/management/commands/mapit_UK_update_ons_ids.py +++ b/mapit_gb/management/commands/mapit_UK_update_ons_ids.py @@ -3,7 +3,7 @@ import csv from django.core.management.base import BaseCommand from mapit.models import Area, Generation, CodeType -from psycopg2 import IntegrityError +from django.db import IntegrityError class Command(BaseCommand): diff --git a/mapit_gb/management/commands/mapit_UK_update_ons_ids2.py b/mapit_gb/management/commands/mapit_UK_update_ons_ids2.py index afba95e2..81e69b47 100644 --- a/mapit_gb/management/commands/mapit_UK_update_ons_ids2.py +++ b/mapit_gb/management/commands/mapit_UK_update_ons_ids2.py @@ -4,7 +4,7 @@ import csv from django.core.management.base import BaseCommand from mapit.models import Area, Generation, CodeType -from psycopg2 import IntegrityError +from django.db import IntegrityError class Command(BaseCommand): diff --git a/setup.py b/setup.py index a31ad4e2..c70cb042 100644 --- a/setup.py +++ b/setup.py @@ -51,9 +51,9 @@ def read_file(filename): scripts=['bin/mapit_make_css'], include_package_data=True, install_requires=[ - 'Django >= 4.2, <6.0', + 'Django >= 4.2, <7.0', 'libsass >= 0.13.3', - 'psycopg2', + 'psycopg', 'PyYAML', 'Shapely', 'uk-postcode-utils', diff --git a/tox.ini b/tox.ini index 52cf6e5d..dd0d1d3a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,16 @@ [tox] -envlist = flake8, py310-{4.2,5.2} +envlist = flake8, py313-{4.2,5.2,6} [testenv] commands = flake8: flake8 mapit mapit_gb mapit_it mapit_no mapit_se mapit_za project - py310: python -W all -W ignore::PendingDeprecationWarning -m coverage run --source mapit manage.py test mapit mapit_gb + py313: python -W all -W ignore::PendingDeprecationWarning -m coverage run --source mapit manage.py test mapit mapit_gb deps = - py310: coverage + py313: coverage flake8: flake8 4.2: Django>=4.2,<5.0 5.2: Django>=5.2,<6.0 + 6: Django>=6.0,<7.0 passenv = CFLAGS PYTHONWARNINGS @@ -21,10 +22,11 @@ skip_install = True [gh-actions] python = - 3.10: flake8, py310 + 3.13: flake8, py313 [gh-actions:env] THING_TO_TEST = flake8: flake8 4.2: 4.2 5.2: 5.2 + 6: 6