Skip to content

Commit 6814e24

Browse files
committed
Build python package
1 parent 6937c3c commit 6814e24

File tree

6 files changed

+508
-42
lines changed

6 files changed

+508
-42
lines changed

proj4rs-clib/Makefile.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,33 @@ dependencies = ["build-release", "cbindgen"]
3636

3737
[tasks.release]
3838
dependencies = ["deb"]
39+
40+
41+
[tasks."python.lint"]
42+
command = "ruff"
43+
args = [
44+
"check",
45+
"--output-format", "concise",
46+
"python",
47+
]
48+
49+
50+
[tasks."python.lint-fix"]
51+
command = "ruff"
52+
args = [
53+
"check",
54+
"--preview",
55+
"--fix",
56+
"python",
57+
]
58+
59+
60+
[tasks."python.typing"]
61+
command = "mypy"
62+
args = ["python"]
63+
64+
65+
[tasks."python.test"]
66+
command = "pytest"
67+
args = [ "-v", "python/tests"]
68+
dependencies = ["python.lint", "python.typing"]

proj4rs-clib/pyproject.toml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Use Maturin https://www.maturin.rs/
2+
[build-system]
3+
requires = ["maturin>=1.7,<2.0"]
4+
build-backend = "maturin"
5+
6+
[project]
7+
name = "proj4rs"
8+
requires-python = ">=3.12"
9+
classifiers = [
10+
"Programming Language :: Rust",
11+
"Programming Language :: Python :: Implementation :: CPython",
12+
"Programming Language :: Python :: Implementation :: PyPy",
13+
]
14+
dependencies = ["cffi"]
15+
dynamic = ["version"]
16+
17+
[tool.maturin]
18+
bindings = "cffi"
19+
python-source = "python"
20+
module-name = "proj4rs._proj4rs"
21+
22+
[tool.ruff]
23+
# Ruff configuration
24+
# See https://docs.astral.sh/ruff/configuration/
25+
line-length = 120
26+
target-version = "py312"
27+
extend-exclude = ["python/proj4rs/_proj4rs"]
28+
29+
[tool.ruff.format]
30+
indent-style = "space"
31+
32+
[tool.ruff.lint]
33+
extend-select = ["E", "F", "I", "ANN", "W", "T", "COM", "RUF"]
34+
ignore = ["ANN002", "ANN003"]
35+
36+
[tool.ruff.lint.per-file-ignores]
37+
"python/tests/*" = ["T201"]
38+
39+
[tool.ruff.lint.isort]
40+
lines-between-types = 1
41+
42+
[tool.ruff.lint.flake8-annotations]
43+
ignore-fully-untyped = true
44+
suppress-none-returning = true
45+
suppress-dummy-args = true
46+
47+
[tool.mypy]
48+
python_version = "3.12"
49+
allow_redefinition = true
50+
51+
[[tool.mypy.overrides]]
52+
module = "_cffi_backend"
53+
ignore_missing_imports = true

proj4rs-clib/python/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
proj4rs/_proj4rs
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
from array import array
2+
from collections import abc
3+
from typing import Any, Tuple, TypeVar, Union, overload
4+
5+
from ._proj4rs import ffi, lib
6+
7+
8+
class Proj:
9+
10+
def __init__(self, defn: str):
11+
_defn = ffi.new("char[]", defn.encode())
12+
self._cdata = lib.proj4rs_proj_new(_defn)
13+
14+
def __del__(self):
15+
lib.proj4rs_proj_delete(self._cdata)
16+
17+
@property
18+
def projname(self) -> str:
19+
_rv = lib.proj4rs_proj_projname(self._cdata)
20+
return ffi.string(_rv).decode()
21+
22+
@property
23+
def is_latlong(self) -> bool:
24+
return lib.proj4rs_proj_is_latlong(self._cdata)
25+
26+
@property
27+
def is_geocent(self) -> bool:
28+
return lib.proj4rs_proj_is_geocent(self._cdata)
29+
30+
@property
31+
def axis(self) -> bytes:
32+
_rv = lib.proj4rs_proj_axis(self._cdata)
33+
return bytes(ffi.cast("uint8_t[3]", _rv))
34+
35+
@property
36+
def is_normalized_axis(self) -> bool:
37+
return lib.proj4rs_proj_is_normalized_axis(self._cdata)
38+
39+
@property
40+
def to_meter(self) -> float:
41+
return lib.proj4rs_proj_to_meter(self._cdata)
42+
43+
@property
44+
def units(self) -> str:
45+
_rv = lib.proj4rs_proj_units(self._cdata)
46+
return ffi.string(_rv).decode()
47+
48+
49+
def _scalar_to_buffer(x):
50+
return array("d", (float(x),))
51+
52+
53+
def _copy_buffer(x, inplace):
54+
match x:
55+
case array():
56+
if not inplace or x.typecode != 'd':
57+
x = array("d", x)
58+
case memoryview():
59+
# Ensure 1 dimensional data
60+
if x.ndim != 1:
61+
raise ValueError("Expecting 1 dimensional array")
62+
if not inplace or x.format != 'd':
63+
x = array("d", x)
64+
case abc.Sequence():
65+
x = array("d", x)
66+
case _:
67+
raise ValueError("Invalid buffer type")
68+
return x
69+
70+
71+
SIZEOF_DOUBLE = ffi.sizeof("double")
72+
73+
74+
T = TypeVar('T')
75+
76+
77+
class Transform:
78+
79+
def __init__(self, src: Proj | str, dst: Proj | str):
80+
self._from = Proj(src) if isinstance(src, str) else src
81+
self._to = Proj(dst) if isinstance(dst, str) else dst
82+
83+
@property
84+
def source(self) -> Proj:
85+
return self._from
86+
87+
@property
88+
def destination(self) -> Proj:
89+
return self._to
90+
91+
@overload
92+
def transform(
93+
self,
94+
x: abc.Buffer,
95+
*,
96+
convert: bool = True,
97+
inplace: bool = False,
98+
) -> Union[
99+
Tuple[abc.Buffer, abc.Buffer],
100+
Tuple[abc.Buffer, abc.Buffer, abc.Buffer],
101+
]: ...
102+
103+
@overload
104+
def transform(
105+
self,
106+
x: float | int,
107+
y: float | int,
108+
*,
109+
convert: bool = True,
110+
inplace: bool = False,
111+
) -> Union[
112+
Tuple[float, float],
113+
]: ...
114+
115+
@overload
116+
def transform(
117+
self,
118+
x: float | int,
119+
y: float | int,
120+
z: float | int,
121+
*,
122+
convert: bool = True,
123+
inplace: bool = False,
124+
) -> Union[
125+
Tuple[float, float, float],
126+
]: ...
127+
128+
@overload
129+
def transform(
130+
self,
131+
x: list | tuple,
132+
y: list | tuple,
133+
*,
134+
convert: bool = True,
135+
inplace: bool = False,
136+
) -> Union[
137+
Tuple[array, array],
138+
]: ...
139+
140+
@overload
141+
def transform(
142+
self,
143+
x: list | tuple,
144+
y: list | tuple,
145+
z: list | tuple,
146+
*,
147+
convert: bool = True,
148+
inplace: bool = False,
149+
) -> Union[
150+
Tuple[array, array, array],
151+
]: ...
152+
153+
@overload
154+
def transform(
155+
self,
156+
x: abc.Buffer,
157+
y: abc.Buffer,
158+
*,
159+
convert: bool = True,
160+
inplace: bool = False,
161+
) -> Union[
162+
Tuple[abc.Buffer, abc.Buffer],
163+
]: ...
164+
165+
@overload
166+
def transform(
167+
self,
168+
x: abc.Buffer,
169+
y: abc.Buffer,
170+
z: abc.Buffer,
171+
*,
172+
convert: bool = True,
173+
inplace: bool = False,
174+
) -> Union[
175+
Tuple[abc.Buffer, abc.Buffer, abc.Buffer],
176+
]: ...
177+
178+
def transform(
179+
self,
180+
x: T,
181+
y: T | None = None,
182+
z: T | None = None,
183+
*,
184+
convert: bool = True,
185+
inplace: bool = False,
186+
) -> Union[
187+
Tuple[Any, Any],
188+
Tuple[Any, Any, Any],
189+
]:
190+
""" Transform coordinates
191+
192+
Parameters
193+
----------
194+
195+
xx: scalar or sequence, input x coordinate(s)
196+
yy: scalar or sequence, optional, input x coordinate(s)
197+
zz: scalar or sequence, optional, input x coordinate(s)
198+
199+
convert: if true, assume that coordinates are in degrees and the transformation
200+
will convert data accordingly
201+
inplace: if true, convert data inplace if the input data implement the Buffer
202+
protocol. The buffer must be writable
203+
204+
Returns
205+
-------
206+
A tuple of buffer objects in the case the input is a Sequence,
207+
a tuple of float otherwise.
208+
209+
If inplace is true and input is a Buffer, the input object is returned.
210+
"""
211+
match (x, y, z):
212+
case (abc.Buffer(), None, None):
213+
scalar = False
214+
m = memoryview(x)
215+
if m.ndim != 2:
216+
raise ValueError("Expecting two-dimensional buffer")
217+
if m.shape is None:
218+
raise ValueError("Invalid buffer shape (None)")
219+
size, dim = m.shape
220+
if dim != 2 and dim != 3:
221+
raise ValueError(f"Expecting geometry dimensions of 2 or 3, found {dim}")
222+
# Flatten buffer
223+
flatten = m.cast('b').cast(m.format) # type: ignore [call-overload]
224+
if not inplace or m.format != 'd' or not m.c_contiguous:
225+
_x = array('d', flatten[0::dim])
226+
_y = array('d', flatten[1::dim])
227+
_z = array('d', flatten[2::dim]) if dim > 2 else None
228+
else:
229+
_x = flatten[0::dim]
230+
_y = flatten[1::dim]
231+
_z = flatten[2::dim] if dim > 2 else None
232+
233+
stride = dim * SIZEOF_DOUBLE
234+
235+
case (abc.Sequence(), abc.Sequence(), _):
236+
scalar = False
237+
if len(y) != len(x) and (not z or len(z) != len(x)): # type: ignore [arg-type]
238+
raise ValueError("Arrays must have the same length")
239+
_x = _copy_buffer(x, inplace)
240+
_y = _copy_buffer(y, inplace)
241+
_z = _copy_buffer(z, inplace) if z else None
242+
size = len(_x)
243+
stride = SIZEOF_DOUBLE
244+
case (abc.Buffer(), abc.Buffer(), _):
245+
scalar = False
246+
mx = memoryview(x)
247+
my = memoryview(y)
248+
mz = memoryview(z) if z else None # type: ignore [arg-type]
249+
if len(my) != len(mx) and (not mz or len(mz) != len(mx)): #
250+
raise ValueError("Buffers must have same length")
251+
_x = _copy_buffer(mx, inplace)
252+
_y = _copy_buffer(my, inplace)
253+
_z = _copy_buffer(mz, inplace) if mz else None
254+
size = len(_x)
255+
stride = SIZEOF_DOUBLE
256+
case _:
257+
scalar = True
258+
_x = _scalar_to_buffer(x)
259+
_y = _scalar_to_buffer(y)
260+
_z = _scalar_to_buffer(z) if z else None
261+
size = 1
262+
stride = SIZEOF_DOUBLE
263+
264+
_t = "double[]"
265+
266+
_xx = ffi.from_buffer(_t, _x, require_writable=True)
267+
_yy = ffi.from_buffer(_t, _y, require_writable=True)
268+
_zz = ffi.from_buffer(_t, _z, require_writable=True) if z else ffi.NULL
269+
res = lib.proj4rs_transform(
270+
self._from._cdata,
271+
self._to._cdata,
272+
_xx,
273+
_yy,
274+
_zz,
275+
size,
276+
stride,
277+
convert,
278+
)
279+
if res != 1:
280+
error = lib.proj4rs_last_error()
281+
raise RuntimeError(ffi.string(error).decode())
282+
283+
if scalar:
284+
return (_x[0], _y[0], _z[0]) if _z else (_x[0], _y[0])
285+
else:
286+
return (_x, _y, _z) if _z else (_x, _y)

0 commit comments

Comments
 (0)