unitforge 0.3.9__tar.gz → 0.3.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unitforge-0.3.9 → unitforge-0.3.11}/.gitignore +1 -0
- unitforge-0.3.11/.gitlab-ci.yml +87 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/Cargo.lock +9 -2
- {unitforge-0.3.9 → unitforge-0.3.11}/Cargo.toml +5 -4
- {unitforge-0.3.9 → unitforge-0.3.11}/PKG-INFO +3 -1
- {unitforge-0.3.9 → unitforge-0.3.11}/pyproject.toml +2 -0
- unitforge-0.3.11/python/unitforge/__init__.py +5 -0
- unitforge-0.3.11/scripts/generate_python_stubs.py +502 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/impl_macros.rs +1 -1
- {unitforge-0.3.9 → unitforge-0.3.11}/src/lib.rs +4 -4
- {unitforge-0.3.9 → unitforge-0.3.11}/src/relations.rs +2 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/bindings/matrix3_py.template.rs +1 -1
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/bindings/vector3_py.template.rs +29 -6
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/test_small_linalg.py +14 -1
- unitforge-0.3.11/tests/typecheck/mypy_smoke.py +48 -0
- unitforge-0.3.11/unitforge_macros/Cargo.toml +15 -0
- unitforge-0.3.9/build.rs → unitforge-0.3.11/unitforge_macros/src/lib.rs +208 -94
- unitforge-0.3.9/.gitlab-ci.yml +0 -49
- {unitforge-0.3.9 → unitforge-0.3.11}/Readme.md +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/Readme_py.md +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/prelude.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/acceleration.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/angle.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/angular_acceleration.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/angular_velocity.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/area.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/area_of_moment.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/charge.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/compliance.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/density.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/distance.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_area.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_distance.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_div_distance_power_four.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_per_volume.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_stress.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/force_volume.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/inverse_area.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/inverse_distance.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/inverse_stress.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/mass.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/mass_div_time_squared_volume.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/mass_per_distance.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/rotational_stiffness.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/stiffness.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/strain.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/stress.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/stress_squared.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/time.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/velocity.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/velocity_squared.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/voltage.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities/volume.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/quantities.template.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/matrix2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/matrix2x3.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/matrix3.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/matrix3x2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/vector2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/small_linalg/vector3.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/tests/quantity_unit_tests.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/src/tests/serde_tests.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/matrix2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/matrix2x3.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/matrix3.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/matrix3x2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/ndarray_dot.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/quantities.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/small_linalg_interfaces.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/test_quantities.py +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/vector2.rs +0 -0
- {unitforge-0.3.9 → unitforge-0.3.11}/tests/vector3.rs +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
image: "rust:latest"
|
|
2
|
+
|
|
3
|
+
lint:
|
|
4
|
+
script:
|
|
5
|
+
- rustup component add clippy
|
|
6
|
+
- cargo clippy --no-deps -- -D warnings
|
|
7
|
+
allow_failure: true
|
|
8
|
+
|
|
9
|
+
lint_serde:
|
|
10
|
+
script:
|
|
11
|
+
- rustup component add clippy
|
|
12
|
+
- cargo clippy -F serde --no-deps -- -D warnings
|
|
13
|
+
allow_failure: true
|
|
14
|
+
|
|
15
|
+
lint_strum:
|
|
16
|
+
script:
|
|
17
|
+
- rustup component add clippy
|
|
18
|
+
- cargo clippy -F strum --no-deps -- -D warnings
|
|
19
|
+
allow_failure: true
|
|
20
|
+
|
|
21
|
+
lint_pyo3:
|
|
22
|
+
script:
|
|
23
|
+
- rustup component add clippy
|
|
24
|
+
- cargo clippy -F pyo3 --no-deps -- -D warnings
|
|
25
|
+
allow_failure: true
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
script:
|
|
29
|
+
- rustc --version && cargo --version
|
|
30
|
+
- cargo test --workspace --verbose
|
|
31
|
+
|
|
32
|
+
test_serde:
|
|
33
|
+
script:
|
|
34
|
+
- rustc --version && cargo --version
|
|
35
|
+
- cargo test -F serde --workspace
|
|
36
|
+
|
|
37
|
+
test_strum:
|
|
38
|
+
script:
|
|
39
|
+
- cargo test -F strum --workspace
|
|
40
|
+
|
|
41
|
+
test_pyo3:
|
|
42
|
+
script:
|
|
43
|
+
- apt-get update && apt-get install -y python3 python3-venv python3.13-dev
|
|
44
|
+
- python3 -m venv .venv
|
|
45
|
+
- source .venv/bin/activate
|
|
46
|
+
- pip install --upgrade pip setuptools wheel maturin pytest numpy
|
|
47
|
+
- python scripts/generate_python_stubs.py
|
|
48
|
+
- maturin develop --features pyo3
|
|
49
|
+
- pytest tests
|
|
50
|
+
- deactivate
|
|
51
|
+
|
|
52
|
+
typecheck_pyo3_stubs:
|
|
53
|
+
script:
|
|
54
|
+
- apt-get update && apt-get install -y python3 python3-venv python3.13-dev
|
|
55
|
+
- python3 -m venv .venv
|
|
56
|
+
- source .venv/bin/activate
|
|
57
|
+
- pip install --upgrade pip setuptools wheel maturin numpy "mypy==1.8.0"
|
|
58
|
+
- python scripts/generate_python_stubs.py
|
|
59
|
+
- python scripts/generate_python_stubs.py --check
|
|
60
|
+
- MYPYPATH=python mypy --python-version 3.7 tests/typecheck/mypy_smoke.py
|
|
61
|
+
- maturin build --features pyo3 --out dist
|
|
62
|
+
- |
|
|
63
|
+
python - <<'PY'
|
|
64
|
+
import glob
|
|
65
|
+
import zipfile
|
|
66
|
+
|
|
67
|
+
wheels = sorted(glob.glob("dist/*.whl"))
|
|
68
|
+
if not wheels:
|
|
69
|
+
raise SystemExit("No wheels built in dist/")
|
|
70
|
+
|
|
71
|
+
required = {
|
|
72
|
+
"unitforge/__init__.py",
|
|
73
|
+
"unitforge/__init__.pyi",
|
|
74
|
+
"unitforge/py.typed",
|
|
75
|
+
}
|
|
76
|
+
with zipfile.ZipFile(wheels[-1]) as wheel:
|
|
77
|
+
names = set(wheel.namelist())
|
|
78
|
+
missing = sorted(required - names)
|
|
79
|
+
if missing:
|
|
80
|
+
raise SystemExit(f"Wheel is missing required typing artifacts: {missing}")
|
|
81
|
+
print("Wheel typing artifacts verified.")
|
|
82
|
+
PY
|
|
83
|
+
- pip install --force-reinstall --no-deps dist/*.whl
|
|
84
|
+
- TMP_DIR="$(mktemp -d)"
|
|
85
|
+
- cp tests/typecheck/mypy_smoke.py "$TMP_DIR"/
|
|
86
|
+
- (cd "$TMP_DIR" && mypy --python-version 3.7 mypy_smoke.py)
|
|
87
|
+
- deactivate
|
|
@@ -369,15 +369,22 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
|
|
369
369
|
|
|
370
370
|
[[package]]
|
|
371
371
|
name = "unitforge"
|
|
372
|
-
version = "0.3.
|
|
372
|
+
version = "0.3.11"
|
|
373
373
|
dependencies = [
|
|
374
374
|
"ndarray",
|
|
375
375
|
"num-traits",
|
|
376
376
|
"numpy",
|
|
377
377
|
"pyo3",
|
|
378
|
-
"regex",
|
|
379
378
|
"serde",
|
|
380
379
|
"serde_json",
|
|
381
380
|
"strum",
|
|
382
381
|
"strum_macros",
|
|
382
|
+
"unitforge_macros",
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
[[package]]
|
|
386
|
+
name = "unitforge_macros"
|
|
387
|
+
version = "0.3.11"
|
|
388
|
+
dependencies = [
|
|
389
|
+
"regex",
|
|
383
390
|
]
|
|
@@ -4,7 +4,7 @@ crate-type = ["cdylib", "rlib"]
|
|
|
4
4
|
|
|
5
5
|
[package]
|
|
6
6
|
name = "unitforge"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.11"
|
|
8
8
|
edition = "2021"
|
|
9
9
|
authors = ["Dr.Q team <info@askdrq.com>", "Henrik Stromberg <henrik@askdrq.com>"]
|
|
10
10
|
description = "A library for unit and quantity consistent computations in Rust"
|
|
@@ -23,11 +23,12 @@ pyo3 = { version = "0.26.0", features = ["extension-module", "auto-initialize",
|
|
|
23
23
|
numpy = { version = "0.26.0", optional = true }
|
|
24
24
|
strum = { version = "0.27.2", optional = true }
|
|
25
25
|
strum_macros = { version = "0.27.2", optional = true }
|
|
26
|
-
|
|
27
|
-
[build-dependencies]
|
|
28
|
-
regex = "1.11.1"
|
|
26
|
+
unitforge_macros = { version = "0.3.10", path = "unitforge_macros" }
|
|
29
27
|
|
|
30
28
|
[features]
|
|
31
29
|
serde = ["dep:serde", "dep:serde_json", "ndarray/serde"]
|
|
32
30
|
pyo3 = ["dep:pyo3", "dep:numpy"]
|
|
33
31
|
strum = ["dep:strum", "dep:strum_macros"]
|
|
32
|
+
|
|
33
|
+
[workspace]
|
|
34
|
+
members = ["unitforge_macros"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitforge
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.11
|
|
4
4
|
Classifier: Programming Language :: Python :: 3
|
|
5
5
|
Classifier: Programming Language :: Rust
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -9,7 +9,9 @@ Classifier: Topic :: Scientific/Engineering :: Physics
|
|
|
9
9
|
Requires-Dist: numpy>=2.2.0
|
|
10
10
|
Summary: A library for unit and quantity consistent computations in Rust
|
|
11
11
|
Keywords: units,quantities,simulation,physics
|
|
12
|
+
Author: Dr.Q team <info@askdrq.com>, Henrik Stromberg <henrik@askdrq.com>
|
|
12
13
|
Author-email: "Dr.Q team" <info@askdrq.com>, Henrik Stromberg <henrik@askdrq.com>
|
|
14
|
+
License: MIT
|
|
13
15
|
Requires-Python: >=3.7
|
|
14
16
|
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
15
17
|
Project-URL: Repository, https://gitlab.com/henrikjstromberg/unitforge
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate mypy-friendly Python stubs for the unitforge package."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import difflib
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
13
|
+
|
|
14
|
+
IMPL_QUANTITY_RE = re.compile(
|
|
15
|
+
r"impl_quantity!\(\s*(?P<quantity>\w+)\s*,\s*(?P<unit>\w+)\s*,",
|
|
16
|
+
re.MULTILINE,
|
|
17
|
+
)
|
|
18
|
+
IMPL_CONST_RE = re.compile(r"impl_const!\(\s*(?P<quantity>\w+)\s*,\s*(?P<name>\w+)\s*,")
|
|
19
|
+
MAKE_ALIAS_RE = re.compile(
|
|
20
|
+
r"make_alias!\(\s*(?P<base_quantity>\w+)\s*,\s*(?P<base_unit>\w+)\s*,\s*"
|
|
21
|
+
r"(?P<alias_quantity>\w+)\s*,\s*(?P<alias_unit>\w+)\s*\)"
|
|
22
|
+
)
|
|
23
|
+
IMPL_MUL_REL_SELF_RE = re.compile(
|
|
24
|
+
r"impl_mul_relation_with_self!\(\s*(?P<lhs>\w+)\s*,\s*(?P<res>\w+)\s*\)"
|
|
25
|
+
)
|
|
26
|
+
IMPL_MUL_REL_OTHER_RE = re.compile(
|
|
27
|
+
r"impl_mul_relation_with_other!\(\s*(?P<lhs>\w+)\s*,\s*(?P<rhs>\w+)\s*,\s*(?P<res>\w+)\s*\)"
|
|
28
|
+
)
|
|
29
|
+
IMPL_SQRT_RE = re.compile(r"impl_sqrt!\(\s*(?P<lhs>\w+)\s*,\s*(?P<res>\w+)\s*\)")
|
|
30
|
+
IMPL_MUL_RE = re.compile(
|
|
31
|
+
r"impl_mul!\(\s*(?P<lhs>\w+)\s*,\s*(?P<rhs>\w+)\s*,\s*(?P<res>\w+)\s*\)"
|
|
32
|
+
)
|
|
33
|
+
IMPL_DIV_RE = re.compile(
|
|
34
|
+
r"impl_div!\(\s*(?P<lhs>\w+)\s*,\s*(?P<rhs>\w+)\s*,\s*(?P<res>\w+)\s*\)"
|
|
35
|
+
)
|
|
36
|
+
IMPL_MUL_WITH_SELF_RE = re.compile(
|
|
37
|
+
r"impl_mul_with_self!\(\s*(?P<lhs>\w+)\s*,\s*(?P<res>\w+)\s*\)"
|
|
38
|
+
)
|
|
39
|
+
IMPL_DIV_SELF_TO_F64_RE = re.compile(r"impl_div_with_self_to_f64!\(\s*(?P<lhs>\w+)\s*\)")
|
|
40
|
+
RTRUE_DIV_RE = re.compile(
|
|
41
|
+
r"fn\s+__rtruediv__\s*\(\s*rhs:\s*PyRef<Self>\s*,\s*lhs:\s*f64\s*\)\s*->\s*PyResult<(?P<res>\w+)>"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class QuantityInfo:
|
|
47
|
+
name: str
|
|
48
|
+
unit: str
|
|
49
|
+
unit_variants: List[str]
|
|
50
|
+
constants: List[str]
|
|
51
|
+
rtruediv_return: Optional[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class OperatorMaps:
|
|
56
|
+
mul: Dict[str, Dict[str, str]]
|
|
57
|
+
div: Dict[str, Dict[str, str]]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _strip_line_comment(line: str) -> str:
|
|
61
|
+
return line.split("//", 1)[0].strip()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_braced_block(source: str, start_index: int) -> str:
|
|
65
|
+
brace_start = source.find("{", start_index)
|
|
66
|
+
if brace_start == -1:
|
|
67
|
+
raise ValueError("opening brace not found")
|
|
68
|
+
|
|
69
|
+
depth = 0
|
|
70
|
+
for index in range(brace_start, len(source)):
|
|
71
|
+
char = source[index]
|
|
72
|
+
if char == "{":
|
|
73
|
+
depth += 1
|
|
74
|
+
elif char == "}":
|
|
75
|
+
depth -= 1
|
|
76
|
+
if depth == 0:
|
|
77
|
+
return source[brace_start + 1 : index]
|
|
78
|
+
raise ValueError("closing brace not found")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_enum_variants(source: str, enum_name: str) -> List[str]:
|
|
82
|
+
enum_match = re.search(rf"pub enum\s+{re.escape(enum_name)}\s*\{{", source)
|
|
83
|
+
if not enum_match:
|
|
84
|
+
raise ValueError("enum not found")
|
|
85
|
+
|
|
86
|
+
body = _extract_braced_block(source, enum_match.start())
|
|
87
|
+
variants: List[str] = []
|
|
88
|
+
|
|
89
|
+
for raw_line in body.splitlines():
|
|
90
|
+
line = _strip_line_comment(raw_line)
|
|
91
|
+
if not line or line.startswith("#"):
|
|
92
|
+
continue
|
|
93
|
+
for segment in line.split(","):
|
|
94
|
+
token = segment.strip()
|
|
95
|
+
if not token:
|
|
96
|
+
continue
|
|
97
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", token):
|
|
98
|
+
variants.append(token)
|
|
99
|
+
return variants
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_quantity_file(path: Path) -> Optional[QuantityInfo]:
|
|
103
|
+
source = path.read_text(encoding="utf-8")
|
|
104
|
+
quantity_match = IMPL_QUANTITY_RE.search(source)
|
|
105
|
+
if quantity_match is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
quantity_name = quantity_match.group("quantity")
|
|
109
|
+
unit_name = quantity_match.group("unit")
|
|
110
|
+
unit_variants = parse_enum_variants(source, unit_name)
|
|
111
|
+
|
|
112
|
+
constants: List[str] = []
|
|
113
|
+
for const_match in IMPL_CONST_RE.finditer(source):
|
|
114
|
+
if const_match.group("quantity") == quantity_name:
|
|
115
|
+
constants.append(const_match.group("name"))
|
|
116
|
+
|
|
117
|
+
rtruediv_match = RTRUE_DIV_RE.search(source)
|
|
118
|
+
rtruediv_return = rtruediv_match.group("res") if rtruediv_match else None
|
|
119
|
+
|
|
120
|
+
return QuantityInfo(
|
|
121
|
+
name=quantity_name,
|
|
122
|
+
unit=unit_name,
|
|
123
|
+
unit_variants=unit_variants,
|
|
124
|
+
constants=constants,
|
|
125
|
+
rtruediv_return=rtruediv_return,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_quantities(quantities_dir: Path) -> List[QuantityInfo]:
|
|
130
|
+
quantities: List[QuantityInfo] = []
|
|
131
|
+
for path in sorted(quantities_dir.glob("*.rs")):
|
|
132
|
+
parsed = parse_quantity_file(path)
|
|
133
|
+
if parsed is not None:
|
|
134
|
+
quantities.append(parsed)
|
|
135
|
+
quantities.sort(key=lambda item: item.name)
|
|
136
|
+
return quantities
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_aliases(quantities_dir: Path) -> List[Tuple[str, str, str, str]]:
|
|
140
|
+
aliases: List[Tuple[str, str, str, str]] = []
|
|
141
|
+
seen = set()
|
|
142
|
+
|
|
143
|
+
for path in sorted(quantities_dir.glob("*.rs")):
|
|
144
|
+
source = path.read_text(encoding="utf-8")
|
|
145
|
+
for match in MAKE_ALIAS_RE.finditer(source):
|
|
146
|
+
alias = (
|
|
147
|
+
match.group("base_quantity"),
|
|
148
|
+
match.group("base_unit"),
|
|
149
|
+
match.group("alias_quantity"),
|
|
150
|
+
match.group("alias_unit"),
|
|
151
|
+
)
|
|
152
|
+
if alias not in seen:
|
|
153
|
+
seen.add(alias)
|
|
154
|
+
aliases.append(alias)
|
|
155
|
+
|
|
156
|
+
aliases.sort(key=lambda alias: (alias[2], alias[3]))
|
|
157
|
+
return aliases
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def parse_sqrt_map(src_root: Path) -> Dict[str, str]:
|
|
161
|
+
sqrt_map: Dict[str, str] = {}
|
|
162
|
+
for path in sorted(src_root.rglob("*.rs")):
|
|
163
|
+
source = path.read_text(encoding="utf-8")
|
|
164
|
+
for match in IMPL_MUL_REL_SELF_RE.finditer(source):
|
|
165
|
+
lhs = match.group("lhs")
|
|
166
|
+
res = match.group("res")
|
|
167
|
+
sqrt_map[res] = lhs
|
|
168
|
+
for match in IMPL_SQRT_RE.finditer(source):
|
|
169
|
+
lhs = match.group("lhs")
|
|
170
|
+
res = match.group("res")
|
|
171
|
+
sqrt_map[lhs] = res
|
|
172
|
+
return sqrt_map
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _add_operator(
|
|
176
|
+
table: Dict[str, Dict[str, str]],
|
|
177
|
+
lhs: str,
|
|
178
|
+
rhs: str,
|
|
179
|
+
result: str,
|
|
180
|
+
) -> None:
|
|
181
|
+
if lhs not in table:
|
|
182
|
+
table[lhs] = {}
|
|
183
|
+
prev = table[lhs].get(rhs)
|
|
184
|
+
if prev is not None and prev != result:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"Conflicting operator mapping for {lhs} and {rhs}: {prev} vs {result}"
|
|
187
|
+
)
|
|
188
|
+
table[lhs][rhs] = result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def parse_operator_maps(src_root: Path) -> OperatorMaps:
|
|
192
|
+
mul: Dict[str, Dict[str, str]] = {}
|
|
193
|
+
div: Dict[str, Dict[str, str]] = {}
|
|
194
|
+
files = [src_root / "relations.rs"] + sorted((src_root / "quantities").glob("*.rs"))
|
|
195
|
+
|
|
196
|
+
for path in files:
|
|
197
|
+
source = path.read_text(encoding="utf-8")
|
|
198
|
+
|
|
199
|
+
for match in IMPL_MUL_REL_OTHER_RE.finditer(source):
|
|
200
|
+
lhs = match.group("lhs")
|
|
201
|
+
rhs = match.group("rhs")
|
|
202
|
+
res = match.group("res")
|
|
203
|
+
_add_operator(mul, lhs, rhs, res)
|
|
204
|
+
_add_operator(mul, rhs, lhs, res)
|
|
205
|
+
_add_operator(div, res, lhs, rhs)
|
|
206
|
+
_add_operator(div, res, rhs, lhs)
|
|
207
|
+
|
|
208
|
+
for match in IMPL_MUL_REL_SELF_RE.finditer(source):
|
|
209
|
+
lhs = match.group("lhs")
|
|
210
|
+
res = match.group("res")
|
|
211
|
+
_add_operator(mul, lhs, lhs, res)
|
|
212
|
+
_add_operator(div, res, lhs, lhs)
|
|
213
|
+
|
|
214
|
+
for match in IMPL_MUL_RE.finditer(source):
|
|
215
|
+
lhs = match.group("lhs")
|
|
216
|
+
rhs = match.group("rhs")
|
|
217
|
+
res = match.group("res")
|
|
218
|
+
_add_operator(mul, lhs, rhs, res)
|
|
219
|
+
_add_operator(mul, rhs, lhs, res)
|
|
220
|
+
|
|
221
|
+
for match in IMPL_DIV_RE.finditer(source):
|
|
222
|
+
lhs = match.group("lhs")
|
|
223
|
+
rhs = match.group("rhs")
|
|
224
|
+
res = match.group("res")
|
|
225
|
+
_add_operator(div, lhs, rhs, res)
|
|
226
|
+
|
|
227
|
+
for match in IMPL_MUL_WITH_SELF_RE.finditer(source):
|
|
228
|
+
lhs = match.group("lhs")
|
|
229
|
+
res = match.group("res")
|
|
230
|
+
_add_operator(mul, lhs, lhs, res)
|
|
231
|
+
|
|
232
|
+
for match in IMPL_DIV_SELF_TO_F64_RE.finditer(source):
|
|
233
|
+
lhs = match.group("lhs")
|
|
234
|
+
_add_operator(div, lhs, lhs, "f64")
|
|
235
|
+
|
|
236
|
+
return OperatorMaps(mul=mul, div=div)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _format_result_annotation(result: str, quantity_names: Set[str]) -> Optional[str]:
|
|
240
|
+
if result == "f64":
|
|
241
|
+
return "float"
|
|
242
|
+
if result in quantity_names:
|
|
243
|
+
return f'"{result}"'
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def render_vector3_stub() -> List[str]:
|
|
248
|
+
return [
|
|
249
|
+
"class Vector3:",
|
|
250
|
+
" def __init__(self, x: Any, y: Any, z: Any, unit: Optional[Any] = ...) -> None: ...",
|
|
251
|
+
" @staticmethod",
|
|
252
|
+
' def zero() -> "Vector3": ...',
|
|
253
|
+
" @staticmethod",
|
|
254
|
+
' def x() -> "Vector3": ...',
|
|
255
|
+
" @staticmethod",
|
|
256
|
+
' def y() -> "Vector3": ...',
|
|
257
|
+
" @staticmethod",
|
|
258
|
+
' def z() -> "Vector3": ...',
|
|
259
|
+
" def __getitem__(self, index: int) -> Any: ...",
|
|
260
|
+
" def __setitem__(self, index: int, value: Any) -> None: ...",
|
|
261
|
+
' def __neg__(self) -> "Vector3": ...',
|
|
262
|
+
' def __add__(self, other: "Vector3") -> "Vector3": ...',
|
|
263
|
+
' def __sub__(self, other: "Vector3") -> "Vector3": ...',
|
|
264
|
+
" def __repr__(self) -> str: ...",
|
|
265
|
+
" def __str__(self) -> str: ...",
|
|
266
|
+
" def to_list(self) -> List[Any]: ...",
|
|
267
|
+
" @staticmethod",
|
|
268
|
+
' def from_list(lst: Sequence[Any], unit: Optional[Any] = ...) -> "Vector3": ...',
|
|
269
|
+
" @staticmethod",
|
|
270
|
+
' def from_array(array: Any, unit: Optional[Any] = ...) -> "Vector3": ...',
|
|
271
|
+
" def to_array(self, unit: Optional[Any] = ...) -> Any: ...",
|
|
272
|
+
" def norm(self) -> Any: ...",
|
|
273
|
+
' def to_unit_vector(self) -> "Vector3": ...',
|
|
274
|
+
' def dot_vec(self, other: "Vector3") -> Any: ...',
|
|
275
|
+
' def cross(self, other: "Vector3") -> "Vector3": ...',
|
|
276
|
+
" def __mul__(self, rhs: Any) -> Vector3: ...",
|
|
277
|
+
" def __truediv__(self, rhs: Any) -> Vector3: ...",
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def render_matrix3_stub() -> List[str]:
|
|
282
|
+
return [
|
|
283
|
+
"class Matrix3:",
|
|
284
|
+
" def __init__(self, data: Optional[Any] = ..., unit: Optional[Any] = ...) -> None: ...",
|
|
285
|
+
" @staticmethod",
|
|
286
|
+
' def zero() -> "Matrix3": ...',
|
|
287
|
+
" @staticmethod",
|
|
288
|
+
' def identity() -> "Matrix3": ...',
|
|
289
|
+
" def __getitem__(self, indices: Tuple[int, int]) -> Any: ...",
|
|
290
|
+
" def __setitem__(self, indices: Tuple[int, int], value: Any) -> None: ...",
|
|
291
|
+
' def __neg__(self) -> "Matrix3": ...',
|
|
292
|
+
" def __repr__(self) -> str: ...",
|
|
293
|
+
" def __str__(self) -> str: ...",
|
|
294
|
+
" @staticmethod",
|
|
295
|
+
' def from_list(lst: Sequence[Sequence[Any]], unit: Optional[Any] = ...) -> "Matrix3": ...',
|
|
296
|
+
" def to_list(self, unit: Optional[Any] = ...) -> List[List[Any]]: ...",
|
|
297
|
+
" @staticmethod",
|
|
298
|
+
' def from_array(array: Any, unit: Optional[Any] = ...) -> "Matrix3": ...',
|
|
299
|
+
" def to_array(self, unit: Optional[Any] = ...) -> Any: ...",
|
|
300
|
+
" @staticmethod",
|
|
301
|
+
' def from_rows(rows: Sequence[Vector3]) -> "Matrix3": ...',
|
|
302
|
+
" @staticmethod",
|
|
303
|
+
' def from_columns(columns: Sequence[Vector3]) -> "Matrix3": ...',
|
|
304
|
+
' def transpose(self) -> "Matrix3": ...',
|
|
305
|
+
" def get_column(self, index: int) -> Vector3: ...",
|
|
306
|
+
" def set_column(self, index: int, value: Vector3) -> None: ...",
|
|
307
|
+
" def get_row(self, index: int) -> Vector3: ...",
|
|
308
|
+
" def set_row(self, index: int, value: Vector3) -> None: ...",
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def render_unit_stub(quantity: QuantityInfo) -> List[str]:
|
|
313
|
+
lines = [f"class {quantity.unit}:"]
|
|
314
|
+
if quantity.unit_variants:
|
|
315
|
+
for variant in quantity.unit_variants:
|
|
316
|
+
lines.append(f' {variant}: ClassVar["{quantity.unit}"]')
|
|
317
|
+
else:
|
|
318
|
+
lines.append(" pass")
|
|
319
|
+
return lines
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def render_quantity_stub(
|
|
323
|
+
quantity: QuantityInfo,
|
|
324
|
+
sqrt_map: Dict[str, str],
|
|
325
|
+
operator_maps: OperatorMaps,
|
|
326
|
+
quantity_names: Set[str],
|
|
327
|
+
) -> List[str]:
|
|
328
|
+
q = quantity.name
|
|
329
|
+
u = quantity.unit
|
|
330
|
+
lines = [
|
|
331
|
+
f"class {q}:",
|
|
332
|
+
f' def __init__(self, value: float, unit: "{u}") -> None: ...',
|
|
333
|
+
" @classmethod",
|
|
334
|
+
f' def zero(cls: Type["{q}"]) -> "{q}": ...',
|
|
335
|
+
f' def to(self, unit: "{u}") -> float: ...',
|
|
336
|
+
f' def close_abs(self, other: "{q}", threshold: "{q}") -> bool: ...',
|
|
337
|
+
f' def close_rel(self, other: "{q}", threshold: float) -> bool: ...',
|
|
338
|
+
f' def __neg__(self) -> "{q}": ...',
|
|
339
|
+
f' def __abs__(self) -> "{q}": ...',
|
|
340
|
+
f' def __add__(self, rhs: "{q}") -> "{q}": ...',
|
|
341
|
+
f' def __sub__(self, rhs: "{q}") -> "{q}": ...',
|
|
342
|
+
" @overload",
|
|
343
|
+
f' def __mul__(self, rhs: float) -> "{q}": ...',
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
mul_ops = operator_maps.mul.get(q, {})
|
|
347
|
+
for rhs in sorted(mul_ops):
|
|
348
|
+
if rhs not in quantity_names:
|
|
349
|
+
continue
|
|
350
|
+
res = _format_result_annotation(mul_ops[rhs], quantity_names)
|
|
351
|
+
if res is None:
|
|
352
|
+
continue
|
|
353
|
+
lines.append(" @overload")
|
|
354
|
+
lines.append(f' def __mul__(self, rhs: "{rhs}") -> {res}: ...')
|
|
355
|
+
lines.append(" @overload")
|
|
356
|
+
lines.append(" def __mul__(self, rhs: Any) -> Any: ...")
|
|
357
|
+
|
|
358
|
+
lines.append(" @overload")
|
|
359
|
+
lines.append(f' def __truediv__(self, rhs: float) -> "{q}": ...')
|
|
360
|
+
div_ops = operator_maps.div.get(q, {})
|
|
361
|
+
for rhs in sorted(div_ops):
|
|
362
|
+
if rhs not in quantity_names:
|
|
363
|
+
continue
|
|
364
|
+
res = _format_result_annotation(div_ops[rhs], quantity_names)
|
|
365
|
+
if res is None:
|
|
366
|
+
continue
|
|
367
|
+
lines.append(" @overload")
|
|
368
|
+
lines.append(f' def __truediv__(self, rhs: "{rhs}") -> {res}: ...')
|
|
369
|
+
lines.append(" @overload")
|
|
370
|
+
lines.append(" def __truediv__(self, rhs: Any) -> Any: ...")
|
|
371
|
+
lines.append(f' def __rmul__(self, rhs: float) -> "{q}": ...')
|
|
372
|
+
lines.append(" def __repr__(self) -> str: ...")
|
|
373
|
+
lines.append(" def __str__(self) -> str: ...")
|
|
374
|
+
|
|
375
|
+
if quantity.rtruediv_return:
|
|
376
|
+
lines.append(f' def __rtruediv__(self, lhs: float) -> "{quantity.rtruediv_return}": ...')
|
|
377
|
+
|
|
378
|
+
for const_name in quantity.constants:
|
|
379
|
+
lines.extend(
|
|
380
|
+
[
|
|
381
|
+
" @staticmethod",
|
|
382
|
+
f' def {const_name}() -> "{q}": ...',
|
|
383
|
+
]
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
sqrt_return = sqrt_map.get(q)
|
|
387
|
+
if sqrt_return:
|
|
388
|
+
lines.append(f' def sqrt(self) -> "{sqrt_return}": ...')
|
|
389
|
+
|
|
390
|
+
return lines
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def render_stub(
|
|
394
|
+
quantities: Sequence[QuantityInfo],
|
|
395
|
+
aliases: Sequence[Tuple[str, str, str, str]],
|
|
396
|
+
sqrt_map: Dict[str, str],
|
|
397
|
+
operator_maps: OperatorMaps,
|
|
398
|
+
) -> str:
|
|
399
|
+
quantity_names = {quantity.name for quantity in quantities}
|
|
400
|
+
|
|
401
|
+
lines: List[str] = [
|
|
402
|
+
"# This file is generated by scripts/generate_python_stubs.py.",
|
|
403
|
+
"# Do not edit manually.",
|
|
404
|
+
"",
|
|
405
|
+
"from typing import Any, ClassVar, List, Optional, Sequence, Tuple, Type, overload",
|
|
406
|
+
"",
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
lines.extend(render_vector3_stub())
|
|
410
|
+
lines.append("")
|
|
411
|
+
lines.extend(render_matrix3_stub())
|
|
412
|
+
lines.append("")
|
|
413
|
+
|
|
414
|
+
for quantity in quantities:
|
|
415
|
+
lines.extend(render_unit_stub(quantity))
|
|
416
|
+
lines.append("")
|
|
417
|
+
lines.extend(
|
|
418
|
+
render_quantity_stub(
|
|
419
|
+
quantity=quantity,
|
|
420
|
+
sqrt_map=sqrt_map,
|
|
421
|
+
operator_maps=operator_maps,
|
|
422
|
+
quantity_names=quantity_names,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
lines.append("")
|
|
426
|
+
|
|
427
|
+
for base_quantity, base_unit, alias_quantity, alias_unit in aliases:
|
|
428
|
+
lines.append(f"{alias_quantity} = {base_quantity}")
|
|
429
|
+
lines.append(f"{alias_unit} = {base_unit}")
|
|
430
|
+
|
|
431
|
+
if aliases:
|
|
432
|
+
lines.append("")
|
|
433
|
+
|
|
434
|
+
return "\n".join(lines)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _print_diff(path: Path, expected: str, current: str) -> None:
|
|
438
|
+
diff = difflib.unified_diff(
|
|
439
|
+
current.splitlines(),
|
|
440
|
+
expected.splitlines(),
|
|
441
|
+
fromfile=f"{path} (current)",
|
|
442
|
+
tofile=f"{path} (expected)",
|
|
443
|
+
lineterm="",
|
|
444
|
+
)
|
|
445
|
+
for line in diff:
|
|
446
|
+
print(line)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def sync_file(path: Path, content: str, check: bool) -> bool:
|
|
450
|
+
if path.exists():
|
|
451
|
+
current = path.read_text(encoding="utf-8")
|
|
452
|
+
else:
|
|
453
|
+
current = None
|
|
454
|
+
|
|
455
|
+
if current == content:
|
|
456
|
+
return False
|
|
457
|
+
|
|
458
|
+
if check:
|
|
459
|
+
print(f"File is out of date: {path}")
|
|
460
|
+
if current is not None:
|
|
461
|
+
_print_diff(path, content, current)
|
|
462
|
+
return True
|
|
463
|
+
|
|
464
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
465
|
+
path.write_text(content, encoding="utf-8")
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def parse_args() -> argparse.Namespace:
|
|
470
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
471
|
+
parser.add_argument(
|
|
472
|
+
"--check",
|
|
473
|
+
action="store_true",
|
|
474
|
+
help="Fail if generated files are missing or outdated.",
|
|
475
|
+
)
|
|
476
|
+
return parser.parse_args()
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def main() -> int:
|
|
480
|
+
args = parse_args()
|
|
481
|
+
repo_root = Path(__file__).resolve().parents[1]
|
|
482
|
+
quantities_dir = repo_root / "src" / "quantities"
|
|
483
|
+
src_root = repo_root / "src"
|
|
484
|
+
out_pkg = repo_root / "python" / "unitforge"
|
|
485
|
+
|
|
486
|
+
quantities = parse_quantities(quantities_dir)
|
|
487
|
+
aliases = parse_aliases(quantities_dir)
|
|
488
|
+
sqrt_map = parse_sqrt_map(src_root)
|
|
489
|
+
operator_maps = parse_operator_maps(src_root)
|
|
490
|
+
stub_content = render_stub(quantities, aliases, sqrt_map, operator_maps)
|
|
491
|
+
|
|
492
|
+
out_of_date = False
|
|
493
|
+
out_of_date |= sync_file(out_pkg / "__init__.pyi", stub_content, args.check)
|
|
494
|
+
out_of_date |= sync_file(out_pkg / "py.typed", "", args.check)
|
|
495
|
+
|
|
496
|
+
if out_of_date and args.check:
|
|
497
|
+
return 1
|
|
498
|
+
return 0
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
sys.exit(main())
|