Skip to content

Commit 3e54f30

Browse files
add tests for building with pip using the shared library (#373)
* add tests for building with pip using the shared library * rework packaging for shared library with python library * add pos methods * rework the method for __format__ * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fine tuning of format for __format__ --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 83b020d commit 3e54f30

File tree

4 files changed

+198
-37
lines changed

4 files changed

+198
-37
lines changed

.github/workflows/pip.yml

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
fail-fast: false
1313
matrix:
1414
platform: [windows-latest, macos-latest, ubuntu-latest, ubuntu-24.04-arm]
15-
python-version: ["3.10", "3.12"]
15+
python-version: ["3.12", "3.13"]
1616

1717
steps:
1818
- uses: actions/checkout@v4
@@ -33,3 +33,65 @@ jobs:
3333
3434
- name: Test
3535
run: python -m pytest ./test/python
36+
37+
build_shared:
38+
name: Build with Pip shared lib
39+
runs-on: ${{ matrix.platform }}
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
platform: [ macos-latest, ubuntu-latest]
44+
python-version: ["3.13"]
45+
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- uses: actions/setup-python@v5
50+
with:
51+
python-version: ${{ matrix.python-version }}
52+
53+
- name: Set min macOS version
54+
if: runner.os == 'macOS'
55+
run: |
56+
echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV
57+
58+
- name: Build and install
59+
run: |
60+
mkdir -p build
61+
cd build
62+
cmake .. -DUNITS_BUILD_SHARED_LIBRARY=ON -DUNITS_BUILD_STATIC_LIBRARY=OFF -DUNITS_INSTALL=ON
63+
make -j 4
64+
sudo make install
65+
cd ..
66+
python -m pip install pytest
67+
pip install -vv -C cmake.define.UNITS_BUILD_SHARED_LIBRARY:BOOL=ON -C cmake.define.UNITS_BUILD_STATIC_LIBRARY:BOOL=OFF .
68+
69+
- name: Test
70+
run: |
71+
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib"
72+
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:/usr/local/lib"
73+
python -m pytest ./test/python
74+
75+
build_shared_windows:
76+
name: Build with Pip shared lib
77+
runs-on: ${{ matrix.platform }}
78+
strategy:
79+
fail-fast: false
80+
matrix:
81+
platform: [windows-latest]
82+
python-version: ["3.12", "3.13"]
83+
84+
steps:
85+
- uses: actions/checkout@v4
86+
87+
- uses: actions/setup-python@v5
88+
with:
89+
python-version: ${{ matrix.python-version }}
90+
91+
- name: Build and install
92+
run: |
93+
python -m pip install pytest
94+
pip install -vv -C cmake.define.UNITS_BUILD_SHARED_LIBRARY:BOOL=ON -C cmake.define.UNITS_BUILD_STATIC_LIBRARY:BOOL=OFF -C cmake.define.UNITS_PYTHON_INSTALL_SHARED_LIBRARY:BOOL=ON .
95+
96+
- name: Test
97+
run: python -m pytest ./test/python

CMakeLists.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,36 @@ if(NOT UNITS_HEADER_ONLY AND UNITS_BUILD_PYTHON_LIBRARY)
266266

267267
# Install directive for scikit-build-core
268268
install(TARGETS units_llnl_ext LIBRARY DESTINATION units_llnl)
269+
if(UNITS_BUILD_SHARED_LIBRARY AND UNITS_PYTHON_INSTALL_SHARED_LIBRARY)
270+
271+
# -------------------------------------------------------------
272+
# setting the RPATH
273+
# -------------------------------------------------------------
274+
if(NOT DEFINED CMAKE_MACOSX_RPATH)
275+
set(CMAKE_MACOSX_RPATH ON)
276+
endif()
277+
278+
# add the automatically determined parts of the RPATH which point to directories
279+
# outside the build tree to the install RPATH
280+
if(NOT DEFINED CMAKE_INSTALL_RPATH_USE_LINK_PATH)
281+
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
282+
endif()
283+
284+
# Add the local directory to the rpath
285+
if(NOT APPLE)
286+
list(APPEND CMAKE_INSTALL_RPATH "$ORIGIN")
287+
else()
288+
list(APPEND CMAKE_INSTALL_RPATH "@loader_path")
289+
list(APPEND CMAKE_INSTALL_RPATH "@executable_path")
290+
endif()
269291

292+
install(
293+
TARGETS units
294+
RUNTIME DESTINATION units_llnl
295+
ARCHIVE DESTINATION units_llnl
296+
LIBRARY DESTINATION units_llnl
297+
)
298+
endif()
270299
endif()
271300

272301
if(UNITS_INSTALL AND NOT SKBUILD)

python/units_python.cpp

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ SPDX-License-Identifier: BSD-3-Clause
1212

1313
#include "units/units.hpp"
1414
#include "units/units_math.hpp"
15+
#include <algorithm>
16+
#include <sstream>
17+
1518
namespace nb = nanobind;
1619

1720
using namespace nb::literals;
@@ -140,35 +143,38 @@ NB_MODULE(units_llnl_ext, mod)
140143
"__mul__",
141144
[](const units::precise_unit& unit,
142145
const std::vector<double>& mult) {
143-
std::vector<units::precise_measurement> results;
144-
results.resize(mult.size());
145-
for (std::size_t ii = 0; ii < mult.size(); ++ii) {
146-
results[ii] = mult[ii] * unit;
147-
}
146+
std::vector<units::precise_measurement> results(mult.size());
147+
std::transform(
148+
mult.begin(),
149+
mult.end(),
150+
results.begin(),
151+
[&unit](double value) { return value * unit; });
148152
return results;
149153
},
150154
nb::is_operator())
151155
.def(
152156
"__rmul__",
153157
[](const units::precise_unit& unit,
154158
const std::vector<double>& mult) {
155-
std::vector<units::precise_measurement> results;
156-
results.resize(mult.size());
157-
for (std::size_t ii = 0; ii < mult.size(); ++ii) {
158-
results[ii] = mult[ii] * unit;
159-
}
159+
std::vector<units::precise_measurement> results(mult.size());
160+
std::transform(
161+
mult.begin(),
162+
mult.end(),
163+
results.begin(),
164+
[&unit](double value) { return value * unit; });
160165
return results;
161166
},
162167
nb::is_operator())
163168
.def(
164169
"__rlshift__",
165170
[](const units::precise_unit& unit,
166171
const std::vector<double>& mult) {
167-
std::vector<units::precise_measurement> results;
168-
results.resize(mult.size());
169-
for (std::size_t ii = 0; ii < mult.size(); ++ii) {
170-
results[ii] = mult[ii] * unit;
171-
}
172+
std::vector<units::precise_measurement> results(mult.size());
173+
std::transform(
174+
mult.begin(),
175+
mult.end(),
176+
results.begin(),
177+
[&unit](double value) { return value * unit; });
172178
return results;
173179
},
174180
nb::is_operator())
@@ -549,25 +555,48 @@ NB_MODULE(units_llnl_ext, mod)
549555
.def(
550556
"__format__",
551557
[](const units::precise_measurement& measurement,
552-
std::string fmt_string) {
558+
std::string fmt_string) -> std::string {
559+
std::string result;
553560
if (fmt_string.empty()) {
554-
return units::to_string(measurement);
555-
}
556-
if (fmt_string == "-") {
557-
return units::to_string(
558-
units::precise_measurement(
559-
measurement.value(), units::precise::one));
560-
}
561-
if (fmt_string.front() == '-') {
562-
return units::to_string(
563-
units::precise_measurement(
564-
measurement.value_as(
565-
units::unit_from_string(fmt_string.substr(1))),
566-
units::precise::one));
561+
result = units::to_string(measurement);
562+
} else if (fmt_string == "-") {
563+
std::stringstream ss;
564+
ss.precision(12);
565+
ss << measurement.value();
566+
result = ss.str();
567+
} else if (fmt_string.front() == '-') {
568+
auto target_unit =
569+
units::unit_from_string(fmt_string.substr(1));
570+
if (!units::is_valid(target_unit)) {
571+
throw std::invalid_argument(
572+
"Invalid unit in format string " +
573+
fmt_string.substr(1));
574+
}
575+
auto new_value = measurement.value_as(target_unit);
576+
if (std::isnan(new_value)) {
577+
throw std::invalid_argument(
578+
"Units are not compatible with given measurement " +
579+
fmt_string.substr(1));
580+
}
581+
std::stringstream ss;
582+
ss.precision(12);
583+
ss << new_value;
584+
result = ss.str();
567585
} else {
568-
return units::to_string(measurement.convert_to(
569-
units::unit_from_string(fmt_string)));
586+
auto target_unit = units::unit_from_string(fmt_string);
587+
if (!units::is_valid(target_unit)) {
588+
throw std::invalid_argument(
589+
"Invalid unit in format string " + fmt_string);
590+
}
591+
auto new_meas = measurement.convert_to(target_unit);
592+
if (!units::is_valid(new_meas)) {
593+
throw std::invalid_argument(
594+
"Units are not compatible with given measurement " +
595+
fmt_string);
596+
}
597+
result = units::to_string(new_meas);
570598
}
599+
return result;
571600
})
572601
.def_prop_ro(
573602
"dimension",
@@ -589,6 +618,11 @@ NB_MODULE(units_llnl_ext, mod)
589618
[](const units::precise_measurement& measurement) {
590619
return -measurement;
591620
})
621+
.def(
622+
"__pos__",
623+
[](const units::precise_measurement& measurement) {
624+
return measurement;
625+
})
592626
.def(
593627
"__invert__",
594628
[](const units::precise_measurement& measurement) {
@@ -707,14 +741,12 @@ NB_MODULE(units_llnl_ext, mod)
707741
.def(
708742
"__rtruediv__",
709743
[](const Dimension& dim1, double val) {
710-
return Dimension{units::precise::one / dim1.base};
744+
return Dimension{dim1.base.inv()};
711745
},
712746
nb::is_operator())
713747
.def(
714748
"__invert__",
715-
[](const Dimension& dim) {
716-
return Dimension{units::precise::one / dim.base};
717-
})
749+
[](const Dimension& dim) { return Dimension{dim.base.inv()}; })
718750
.def(
719751
"__pow__",
720752
[](const Dimension& dim, int power) {

test/python/test_measurement.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ def test_negation():
166166
assert m3.value == -15.0
167167

168168

169+
def test_pos():
170+
m1 = u.Measurement("15 seconds")
171+
m3 = +m1
172+
assert m1 == m3
173+
174+
169175
def test_conditions():
170176
m1 = u.Measurement(34.5, "fq2te1tg1fe")
171177
assert not m1
@@ -259,11 +265,43 @@ def test_format():
259265
assert "kg" in s1
260266

261267
s2 = f"the measurement is {m1:-}"
262-
assert s2 == "the measurement is 9.7552 "
268+
assert s2 == "the measurement is 9.7552"
263269

264270
s3 = f"the measurement is {m1:-kg}"
265271
assert "kg" not in s3
266272

273+
m3 = u.Measurement("3.4")
274+
275+
s4 = f"the measurement is {m3:-}"
276+
assert "3.4" in s4
277+
278+
279+
def test_format_error():
280+
m1 = u.Measurement("9.7552 lb")
281+
try:
282+
s1 = f"the measurement is {m1:A}"
283+
assert False
284+
except ValueError:
285+
assert True
286+
287+
try:
288+
s1 = f"the measurement is {m1:sgsg2362yqbvwsdddddqwadsa}"
289+
assert False
290+
except ValueError:
291+
assert True
292+
293+
try:
294+
s1 = f"the measurement is {m1:-A}"
295+
assert False
296+
except ValueError:
297+
assert True
298+
299+
try:
300+
s1 = f"the measurement is {m1:-sgsg2362yqbvwsdddddqwadsa}"
301+
assert False
302+
except ValueError:
303+
assert True
304+
267305

268306
def test_close():
269307
m1 = u.Measurement("10 lb")

0 commit comments

Comments
 (0)