Skip to content

Commit 18d9d5a

Browse files
committed
feat: add decorator to check if las file is written correctly
fix: tests after adding decorator fix: tests using check_las fix: review fix: review fix: tests fix: tests and doc fix: tests
1 parent 29a38e0 commit 18d9d5a

8 files changed

Lines changed: 146 additions & 6 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# dev
2+
- [new] add check_las and use it when needed
3+
14
# 1.17.0
25
- [new] add function to launch a pdal pipeline on a Las file
36

pdaltools/check_las.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Check that LAS/LAZ files can be read by PDAL.
2+
3+
Provides a one-shot check, a retry variant (for files still being written), and a
4+
decorator that validates an output path after a wrapped function returns.
5+
"""
6+
7+
import functools
8+
import inspect
9+
import os
10+
import time
11+
12+
import pdal
13+
14+
15+
def check_pdal_can_open_file(filepath: str) -> bool:
16+
try:
17+
if not os.path.exists(filepath):
18+
raise FileNotFoundError(f"File {filepath} does not exist.")
19+
pipeline = pdal.Reader.las(filename=filepath).pipeline()
20+
pipeline.execute()
21+
return True
22+
except RuntimeError as e:
23+
if e.__str__().startswith("readers.las:"):
24+
print(f"Pdal could not read {filepath} due to error: {e}")
25+
return False
26+
else:
27+
raise e
28+
29+
30+
def check_pdal_can_open_file_with_retry(filepath: str, delay: int) -> bool:
31+
if check_pdal_can_open_file(filepath):
32+
return True
33+
else:
34+
print(f"New attempt in {delay} seconds.")
35+
time.sleep(delay)
36+
return check_pdal_can_open_file(filepath)
37+
38+
39+
def check_pdal_can_open_file_with_retry_decorator(delay: int, filepath: str | None = None):
40+
def decorator(fn):
41+
sig = inspect.signature(fn)
42+
43+
# Extract the LAS path from the wrapped call: bind args/kwargs to the
44+
# function signature, apply defaults if needed, then return the parameter
45+
# named by `filepath` (e.g. "output_file" or the first positional arg).
46+
def resolve_filepath(*args, **kwargs) -> str:
47+
bound = sig.bind_partial(*args, **kwargs)
48+
if filepath not in bound.arguments:
49+
bound.apply_defaults()
50+
return str(bound.arguments[filepath])
51+
52+
@functools.wraps(fn)
53+
def wrapper(*args, **kwargs):
54+
result = fn(*args, **kwargs)
55+
resolved_filepath = resolve_filepath(*args, **kwargs)
56+
if not check_pdal_can_open_file_with_retry(resolved_filepath, delay):
57+
raise RuntimeError(f"Pdal could not read {resolved_filepath} after retry.")
58+
return result
59+
60+
return wrapper
61+
62+
return decorator

pdaltools/color.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pdal
77

88
import pdaltools.las_info as las_info
9+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
910
from pdaltools.download_image import download_image
1011

1112

@@ -31,6 +32,7 @@ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: f
3132
return min_d, max_d
3233

3334

35+
@check_pdal_can_open_file_with_retry_decorator(delay=10, filepath="output_file")
3436
def color_from_stream(
3537
input_file: str,
3638
output_file: str,
@@ -156,6 +158,7 @@ def color_from_stream(
156158
return tmp_ortho, tmp_ortho_irc
157159

158160

161+
@check_pdal_can_open_file_with_retry_decorator(delay=10, filepath="output_file")
159162
def color_from_files(
160163
input_file: str,
161164
output_file: str,

pdaltools/las_add_extra_dims_from_las.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import laspy
1616
import numpy as np
1717

18+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
19+
1820
_LAS_SUFFIXES = frozenset({".las", ".laz"})
1921
logger = logging.getLogger(__name__)
2022
# ANSI for terminal emphasis (ignored by non-TTY handlers in most setups).
@@ -160,6 +162,7 @@ def _dims_to_copy(base: laspy.LasData, source: laspy.LasData, dimensions: Option
160162
return [d for d in requested if d in missing]
161163

162164

165+
@check_pdal_can_open_file_with_retry_decorator(delay=10, filepath="output_las")
163166
def add_extra_dims_from_las(
164167
base_las: Path | str,
165168
source_las: Path | str,

pdaltools/standardize_format.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import pdal
1717

18+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
1819
from pdaltools.las_rename_dimension import rename_dimension
1920
from pdaltools.unlock_file import copy_and_hack_decorator
2021

@@ -79,16 +80,13 @@ def get_writer_parameters(new_parameters: Dict) -> Dict:
7980

8081

8182
@copy_and_hack_decorator
83+
@check_pdal_can_open_file_with_retry_decorator(delay=10, filepath="output_file")
8284
def standardize(
83-
input_file: str,
84-
output_file: str,
85-
params_from_parser: Dict,
86-
classes_to_remove: List = [],
87-
rename_dims: List = []
85+
input_file: str, output_file: str, params_from_parser: Dict, classes_to_remove: List = [], rename_dims: List = []
8886
) -> None:
8987
"""
9088
Standardize a LAS/LAZ file with improved error handling and resource management.
91-
89+
9290
Args:
9391
input_file: Input file path
9492
output_file: Output file path
450 KB
Binary file not shown.
610 KB
Binary file not shown.

test/test_check_las.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Check if a LAS file is written correcty and can be opened by PDAL"""
2+
3+
import os
4+
import shutil
5+
6+
import pytest
7+
8+
from pdaltools.check_las import (
9+
check_pdal_can_open_file,
10+
check_pdal_can_open_file_with_retry,
11+
check_pdal_can_open_file_with_retry_decorator,
12+
)
13+
14+
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
15+
INPUT_DIR = os.path.join(TEST_PATH, "data/check_las")
16+
17+
18+
def test_check_pdal_can_open_file():
19+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
20+
assert check_pdal_can_open_file(filepath)
21+
22+
23+
def test_check_pdal_can_open_file_nok():
24+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
25+
assert not check_pdal_can_open_file(filepath)
26+
27+
28+
def test_check_pdal_can_open_file_with_retry():
29+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
30+
assert check_pdal_can_open_file_with_retry(filepath, 1)
31+
32+
33+
def test_check_pdal_can_open_file_with_retry_nok():
34+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
35+
assert not check_pdal_can_open_file_with_retry(filepath, 1)
36+
37+
38+
@check_pdal_can_open_file_with_retry_decorator(delay=0, filepath="filepath") # or mock time.sleep
39+
def echo(filepath):
40+
return filepath
41+
42+
43+
def test_decorator_passes_through_on_ok():
44+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
45+
assert echo(filepath) == filepath
46+
47+
48+
def test_decorator_raises_on_nok():
49+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
50+
with pytest.raises(RuntimeError):
51+
echo(filepath)
52+
53+
54+
@check_pdal_can_open_file_with_retry_decorator(delay=0, filepath="output_file")
55+
def copy_las(input_file, output_file):
56+
shutil.copy(input_file, output_file)
57+
58+
59+
def test_decorator_post_write_passes_on_ok(tmp_path):
60+
input_file = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
61+
output_file = tmp_path / "output.laz"
62+
copy_las(input_file, output_file)
63+
print(output_file)
64+
assert output_file.is_file()
65+
66+
67+
def test_decorator_post_write_raises_on_nok(tmp_path):
68+
input_file = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
69+
output_file = tmp_path / "output.laz"
70+
with pytest.raises(RuntimeError):
71+
copy_las(input_file, output_file)

0 commit comments

Comments
 (0)