diff --git a/CLAUDE.md b/CLAUDE.md index 2d91aae..17a3893 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1140,6 +1140,49 @@ three -- `(position, bias, info)` -- inside a `try/except Exception: pass`, and got a confident `0/100 converged`. That is the issue's own defect one level up: a blanket except turning a caller error into a result about the callee. Fourth harness in this file to report the thing it could not read as broken. +## Every example now bootstraps its own sys.path, and the sweep took three tries + +The trap at the top of this file -- `python chX/example.py` resolving `core` to +the main checkout -- is closed at the source now: all 38 examples insert the +repository root before their first `core` import, matching what +`ch5_fingerprinting/example_classification` and nine of twelve `scripts/` +generators already did. `tests/test_examples_import_this_checkout.py` holds it, +and checks *order* rather than presence, because a bootstrap below the import +changes nothing. + +Two things worth knowing before repeating this kind of sweep. + +**Three of the three bugs in the sweep script were found by pyflakes, none by +reading.** Each was a plausible-looking way to locate an insertion point: + +1. `ast.walk` to decide whether `sys` was already imported -- it counts an + `import sys` inside a *function*, so four files got a module-level + `sys.path.insert` with no `sys` bound. +2. Finding the stdlib group with `line.startswith(("import ", "from "))` -- + which matched a line of *module docstring prose* beginning "from range + measurements ...", and wrote `import sys` inside the docstring. +3. Treating any module-level import of a name as sufficient -- ch7's pose-graph + example imports `pathlib.Path` three lines *below* its first `core` import, + so the name existed but not yet at the point the bootstrap runs. + +All three produce files that `compileall` accepts. The rule from the earlier +lint sweep holds and is worth restating in the stronger form: **a tool that +locates Python by line prefix cannot tell an import from prose that starts like +one, and `ast.walk` cannot tell module scope from function scope.** Use +`tree.body`, and compare line numbers. + +**The import-order ratchet fired, correctly, and fixing it improved the +baseline.** Inserting `import sys` at the top of a stdlib group is unsorted, so +I001 went 113 -> 147. `ruff --select I001 --fix` over the chapter directories +cleared 63, including 29 that predated this change, leaving **84** -- so the +baseline moved down, not up. E402 does *not* fire: ruff exempts imports that +follow a `sys.path` manipulation, which is what makes this idiom viable at all. + +Verification was the cheap strong one: `--help` for all 38 examples, captured +before and diffed after, **byte-identical both times** -- once after the +insertions and again after ruff reordered the imports. `--help` exits during +argument parsing but only after every module-level import has run, so it tests +exactly what this change touches, in about a second per example. ## A scratch probe in the scratchpad imports the *other* checkout diff --git a/README.md b/README.md index d206726..d39f429 100644 --- a/README.md +++ b/README.md @@ -155,10 +155,17 @@ python -m ch6_dead_reckoning.example_comparison ``` `python -m` puts the repository root on `sys.path`, so these run straight from -a fresh clone even before step 4 above. The script form — -`python /.py` — puts the *script's* directory there instead, -so `core` is only importable once the package is installed. That is why every -command in this repository is written as `python -m`. +a fresh clone even before step 4 above, and it is the form every command in +this repository is written in. + +The script form — `python /.py` — puts the *script's* +directory there instead, so `core` would not be importable from a fresh clone. +Each example now adds the repository root itself before importing `core`, so +that form works too. It is worth knowing why the line is there: without it, on +a machine that has ever installed this package, `import core` does not fail — +it quietly resolves to **whichever checkout the install points at**, and the +example runs to completion against a different copy of the library. The error, +when there is one, names a directory you have never heard of. Examples find their datasets from any working directory, so `cd`-ing into a chapter folder first is fine. Every example takes `--help`, which prints what diff --git a/ch2_coords/example_attitude_visualization.py b/ch2_coords/example_attitude_visualization.py index 8fd6746..f71e010 100644 --- a/ch2_coords/example_attitude_visualization.py +++ b/ch2_coords/example_attitude_visualization.py @@ -33,11 +33,19 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.coords import ( enu_to_ned, euler_to_rotation_matrix, diff --git a/ch2_coords/example_coordinate_transforms.py b/ch2_coords/example_coordinate_transforms.py index 0b6624d..77fa607 100644 --- a/ch2_coords/example_coordinate_transforms.py +++ b/ch2_coords/example_coordinate_transforms.py @@ -24,11 +24,18 @@ import argparse import json +import sys from pathlib import Path import numpy as np -from core.utils import angle_diff, resolve_data_path +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.coords import ( ecef_to_enu, ecef_to_llh, @@ -41,6 +48,7 @@ quat_to_rotation_matrix, rotation_matrix_to_euler, ) +from core.utils import angle_diff, resolve_data_path def load_dataset(data_dir: str) -> dict: diff --git a/ch3_estimators/example_comparison.py b/ch3_estimators/example_comparison.py index e4d4a9f..c967030 100644 --- a/ch3_estimators/example_comparison.py +++ b/ch3_estimators/example_comparison.py @@ -28,12 +28,28 @@ import argparse import contextlib import io +import sys import time from pathlib import Path -import numpy as np import matplotlib.pyplot as plt +import numpy as np from tqdm import tqdm + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.estimators import ( + ExtendedKalmanFilter, + Factor, + FactorGraph, + ParticleFilter, + UnscentedKalmanFilter, +) from core.eval import ( plot_error_cdf, plot_error_magnitude_time, @@ -41,13 +57,6 @@ save_figure, show_figures_if_requested, ) -from core.estimators import ( - ExtendedKalmanFilter, - UnscentedKalmanFilter, - ParticleFilter, - Factor, - FactorGraph, -) def setup_scenario(seed=42): diff --git a/ch3_estimators/example_ekf_range_bearing.py b/ch3_estimators/example_ekf_range_bearing.py index 15dd551..38e577d 100644 --- a/ch3_estimators/example_ekf_range_bearing.py +++ b/ch3_estimators/example_ekf_range_bearing.py @@ -22,11 +22,20 @@ import argparse import json +import sys from pathlib import Path from typing import Dict -import numpy as np import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.estimators import ExtendedKalmanFilter from core.eval import save_figure, show_figures_if_requested from core.utils import angle_diff, resolve_data_path diff --git a/ch3_estimators/example_iekf_range_bearing.py b/ch3_estimators/example_iekf_range_bearing.py index 88896f8..048ab17 100644 --- a/ch3_estimators/example_iekf_range_bearing.py +++ b/ch3_estimators/example_iekf_range_bearing.py @@ -29,10 +29,18 @@ """ import argparse +import sys from pathlib import Path -import numpy as np import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from core.estimators import ExtendedKalmanFilter, IteratedExtendedKalmanFilter from core.eval import ( diff --git a/ch3_estimators/example_kalman_1d.py b/ch3_estimators/example_kalman_1d.py index 68fa66f..d6d0a4e 100644 --- a/ch3_estimators/example_kalman_1d.py +++ b/ch3_estimators/example_kalman_1d.py @@ -20,10 +20,19 @@ """ import argparse +import sys from pathlib import Path -import numpy as np import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.estimators import KalmanFilter from core.eval import save_figure, show_figures_if_requested diff --git a/ch3_estimators/example_least_squares.py b/ch3_estimators/example_least_squares.py index 563ffd9..e15c955 100644 --- a/ch3_estimators/example_least_squares.py +++ b/ch3_estimators/example_least_squares.py @@ -35,18 +35,27 @@ """ import argparse -import numpy as np -import matplotlib.pyplot as plt +import sys from pathlib import Path -from core.eval import save_figure, show_figures_if_requested +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.estimators import ( - linear_least_squares, - weighted_least_squares, gauss_newton, levenberg_marquardt, + linear_least_squares, robust_gauss_newton, + weighted_least_squares, ) +from core.eval import save_figure, show_figures_if_requested def setup_positioning_scenario(): diff --git a/ch3_estimators/example_particle_bimodal.py b/ch3_estimators/example_particle_bimodal.py index 2764967..ab3e413 100644 --- a/ch3_estimators/example_particle_bimodal.py +++ b/ch3_estimators/example_particle_bimodal.py @@ -44,13 +44,26 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.estimators import ParticleFilter -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) FIGS_DIR = Path(__file__).parent / "figs" diff --git a/ch4_rf_point_positioning/example_aoa_positioning.py b/ch4_rf_point_positioning/example_aoa_positioning.py index 77efba6..e25e1ae 100644 --- a/ch4_rf_point_positioning/example_aoa_positioning.py +++ b/ch4_rf_point_positioning/example_aoa_positioning.py @@ -23,11 +23,19 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import save_figure, show_figures_if_requested from core.rf import ( AOAPositioner, diff --git a/ch4_rf_point_positioning/example_comparison.py b/ch4_rf_point_positioning/example_comparison.py index 10e0ede..9eaf851 100644 --- a/ch4_rf_point_positioning/example_comparison.py +++ b/ch4_rf_point_positioning/example_comparison.py @@ -24,6 +24,7 @@ import argparse import json +import sys import time from functools import partial from pathlib import Path @@ -33,6 +34,13 @@ import numpy as np from tqdm import tqdm +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import save_figure, show_figures_if_requested from core.rf import ( DIVERGENCE_M, diff --git a/ch4_rf_point_positioning/example_dop_geometry.py b/ch4_rf_point_positioning/example_dop_geometry.py index 5de9aed..aa41818 100644 --- a/ch4_rf_point_positioning/example_dop_geometry.py +++ b/ch4_rf_point_positioning/example_dop_geometry.py @@ -43,13 +43,26 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Ellipse -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) from core.rf.dop import compute_dop, compute_geometry_matrix from core.rf.positioning import TOAPositioner diff --git a/ch4_rf_point_positioning/example_tdoa_positioning.py b/ch4_rf_point_positioning/example_tdoa_positioning.py index 1fba458..240df24 100644 --- a/ch4_rf_point_positioning/example_tdoa_positioning.py +++ b/ch4_rf_point_positioning/example_tdoa_positioning.py @@ -15,11 +15,19 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import save_figure, show_figures_if_requested from core.rf import ( TDOAPositioner, @@ -29,8 +37,6 @@ toa_fang_solver, ) - - # Seed for the Monte Carlo in Demo 2. SEED = 42 diff --git a/ch4_rf_point_positioning/example_toa_positioning.py b/ch4_rf_point_positioning/example_toa_positioning.py index 11ccd88..02038c4 100644 --- a/ch4_rf_point_positioning/example_toa_positioning.py +++ b/ch4_rf_point_positioning/example_toa_positioning.py @@ -16,11 +16,19 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import save_figure, show_figures_if_requested from core.rf import ( SPEED_OF_LIGHT, diff --git a/ch5_fingerprinting/example_classification.py b/ch5_fingerprinting/example_classification.py index cb12eac..9ae13a1 100644 --- a/ch5_fingerprinting/example_classification.py +++ b/ch5_fingerprinting/example_classification.py @@ -33,8 +33,6 @@ load_fingerprint_database, ) - - # Seed for this example's synthetic database and query draws. Fixed so the # committed figures and reported accuracies can be regenerated exactly. DEFAULT_SEED = 42 diff --git a/ch5_fingerprinting/example_comparison.py b/ch5_fingerprinting/example_comparison.py index 9d545d1..56f7975 100644 --- a/ch5_fingerprinting/example_comparison.py +++ b/ch5_fingerprinting/example_comparison.py @@ -16,20 +16,29 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import plot_error_cdf, save_figure, show_figures_if_requested from core.fingerprinting import ( - load_fingerprint_database, - nn_localize, - knn_localize, + LinearRegressionLocalizer, fit_gaussian_naive_bayes, + knn_localize, + load_fingerprint_database, map_localize, + nn_localize, posterior_mean_localize, - LinearRegressionLocalizer, ) diff --git a/ch5_fingerprinting/example_deterministic.py b/ch5_fingerprinting/example_deterministic.py index e2bf8a1..0468b37 100644 --- a/ch5_fingerprinting/example_deterministic.py +++ b/ch5_fingerprinting/example_deterministic.py @@ -13,16 +13,25 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import plot_error_cdf, save_figure, show_figures_if_requested from core.fingerprinting import ( + knn_localize, load_fingerprint_database, nn_localize, - knn_localize, ) diff --git a/ch5_fingerprinting/example_pattern_recognition.py b/ch5_fingerprinting/example_pattern_recognition.py index 97efd45..76626fa 100644 --- a/ch5_fingerprinting/example_pattern_recognition.py +++ b/ch5_fingerprinting/example_pattern_recognition.py @@ -13,15 +13,24 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import plot_error_cdf, save_figure, show_figures_if_requested from core.fingerprinting import ( - load_fingerprint_database, LinearRegressionLocalizer, + load_fingerprint_database, ) diff --git a/ch5_fingerprinting/example_probabilistic.py b/ch5_fingerprinting/example_probabilistic.py index 56ed6da..6518b65 100644 --- a/ch5_fingerprinting/example_probabilistic.py +++ b/ch5_fingerprinting/example_probabilistic.py @@ -15,18 +15,27 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import plot_error_cdf, save_figure, show_figures_if_requested from core.fingerprinting import ( - load_fingerprint_database, fit_gaussian_naive_bayes, + load_fingerprint_database, + log_posterior, map_localize, posterior_mean_localize, - log_posterior, ) diff --git a/ch5_fingerprinting/example_walk_posterior.py b/ch5_fingerprinting/example_walk_posterior.py index 1899435..42e4dea 100644 --- a/ch5_fingerprinting/example_walk_posterior.py +++ b/ch5_fingerprinting/example_walk_posterior.py @@ -32,13 +32,26 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np from matplotlib.colors import PowerNorm -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) from core.fingerprinting import ( fit_gaussian_naive_bayes, load_fingerprint_database, diff --git a/ch6_dead_reckoning/example_allan_variance.py b/ch6_dead_reckoning/example_allan_variance.py index 1ba8912..482b4ec 100644 --- a/ch6_dead_reckoning/example_allan_variance.py +++ b/ch6_dead_reckoning/example_allan_variance.py @@ -18,11 +18,20 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested from core.sensors import ( allan_variance, @@ -33,7 +42,6 @@ scale_to_bias_instability, ) - #: Seed for the synthetic record. The generator used a bare #: ``np.random.default_rng()``, so every run drew a different realisation: #: bias instability came out 11.15 deg/hr on one run and 7.85 on the next, a diff --git a/ch6_dead_reckoning/example_comparison.py b/ch6_dead_reckoning/example_comparison.py index 54ed034..418596a 100644 --- a/ch6_dead_reckoning/example_comparison.py +++ b/ch6_dead_reckoning/example_comparison.py @@ -15,6 +15,7 @@ """ import argparse +import sys import time from pathlib import Path from typing import Dict, Optional, Tuple @@ -22,6 +23,13 @@ import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import ( plot_error_cdf, plot_error_magnitude_time, diff --git a/ch6_dead_reckoning/example_environment.py b/ch6_dead_reckoning/example_environment.py index f15f5dc..a9495fa 100644 --- a/ch6_dead_reckoning/example_environment.py +++ b/ch6_dead_reckoning/example_environment.py @@ -18,22 +18,29 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested from core.sensors import ( + detect_floor_change, mag_heading, - wrap_angle_diff, pressure_to_altitude, - detect_floor_change, smooth_measurement_simple, + wrap_angle_diff, ) - - # Seed for this example's random walk and sensor noise. Fixed so the # committed figures can be regenerated exactly. DEFAULT_SEED = 42 diff --git a/ch6_dead_reckoning/example_imu_strapdown.py b/ch6_dead_reckoning/example_imu_strapdown.py index 75850fc..9ddc3c3 100644 --- a/ch6_dead_reckoning/example_imu_strapdown.py +++ b/ch6_dead_reckoning/example_imu_strapdown.py @@ -17,26 +17,33 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path +import matplotlib.pyplot as plt +import numpy as np + # Import Chapter 6 sensor algorithms +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested from core.sensors import ( FrameConvention, IMUNoiseParams, - strapdown_update, NavStateQPVP, + strapdown_update, units, ) # Import IMU forward model from core.sim import generate_imu_from_trajectory - - # Seed for this example's sensor-noise draws. Fixed so the committed # figures can be regenerated exactly; see the noise function below. DEFAULT_SEED = 42 diff --git a/ch6_dead_reckoning/example_pdr.py b/ch6_dead_reckoning/example_pdr.py index 746fa82..768406d 100644 --- a/ch6_dead_reckoning/example_pdr.py +++ b/ch6_dead_reckoning/example_pdr.py @@ -23,30 +23,37 @@ import argparse import json +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path from typing import Dict, Optional +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested -from core.utils import resolve_data_path from core.sensors import ( FrameConvention, IMUNoiseParams, detect_steps_peak_detector, + integrate_gyro_heading, + mag_heading, + pdr_step_update, step_length, step_length_book_eq6_49, step_length_weinberg, - pdr_step_update, - integrate_gyro_heading, - wrap_heading, - mag_heading, units, + wrap_heading, ) from core.sim import generate_imu_from_trajectory - - +from core.utils import resolve_data_path # Seed for this example's sensor-noise draws. Fixed so the committed # figures can be regenerated exactly; see the noise function below. diff --git a/ch6_dead_reckoning/example_wheel_odometry.py b/ch6_dead_reckoning/example_wheel_odometry.py index 42358df..ea00b2e 100644 --- a/ch6_dead_reckoning/example_wheel_odometry.py +++ b/ch6_dead_reckoning/example_wheel_odometry.py @@ -19,15 +19,22 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path -from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested -from core.sensors import wheel_odom_update, NavStateQPVP +import matplotlib.pyplot as plt +import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from core.eval import resolve_figs_dir, save_figure, show_figures_if_requested +from core.sensors import NavStateQPVP, wheel_odom_update # Seed for this example's sensor-noise draws. Fixed so the committed # figures can be regenerated exactly; see the noise function below. diff --git a/ch6_dead_reckoning/example_zupt.py b/ch6_dead_reckoning/example_zupt.py index ab9cde6..a171d55 100644 --- a/ch6_dead_reckoning/example_zupt.py +++ b/ch6_dead_reckoning/example_zupt.py @@ -17,18 +17,32 @@ """ import argparse +import sys import time -import numpy as np -import matplotlib.pyplot as plt from pathlib import Path -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) from core.sensors import ( FrameConvention, IMUNoiseParams, - strapdown_update, - detect_zupt_windowed, NavStateQPVP, + detect_zupt_windowed, + strapdown_update, ) from core.sensors.ins_ekf import ZUPT_EKF from core.sim import generate_imu_from_trajectory diff --git a/ch7_slam/example_bundle_adjustment.py b/ch7_slam/example_bundle_adjustment.py index c473111..094069a 100644 --- a/ch7_slam/example_bundle_adjustment.py +++ b/ch7_slam/example_bundle_adjustment.py @@ -25,19 +25,27 @@ """ import argparse -import numpy as np -import matplotlib.pyplot as plt +import sys from pathlib import Path -from typing import List, Tuple, Dict +from typing import Dict, List, Tuple + +import matplotlib.pyplot as plt +import numpy as np + +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from core.estimators.factor_graph import FactorGraph from core.eval import save_animation, save_figure, show_figures_if_requested from core.slam import ( CameraIntrinsics, - project_point, create_reprojection_factor, + project_point, ) -from core.estimators.factor_graph import FactorGraph - # Standard deviation of the synthetic pixel observations, and the sigma the # reprojection factors are weighted by. Named because the final reprojection diff --git a/ch7_slam/example_pose_graph_slam.py b/ch7_slam/example_pose_graph_slam.py index 6a94a75..193f581 100644 --- a/ch7_slam/example_pose_graph_slam.py +++ b/ch7_slam/example_pose_graph_slam.py @@ -44,26 +44,37 @@ import argparse import json +import sys +from pathlib import Path + import numpy as np -from core.utils import resolve_data_path, wrap_angle +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from pathlib import Path +from typing import Dict, List, Optional, Tuple + import matplotlib.pyplot as plt from matplotlib.transforms import blended_transform_factory -from pathlib import Path -from typing import List, Tuple, Dict, Optional from core.eval import save_animation, save_figure, show_figures_if_requested from core.slam import ( + LoopClosureDetector2D, + SlamFrontend2D, + create_pose_graph, + icp_point_to_point, se2_compose, se2_relative, - icp_point_to_point, - create_pose_graph, - SlamFrontend2D, - LoopClosureDetector2D, ) from core.slam.scan_generation import ( generate_scan_with_occlusion, ) +from core.utils import resolve_data_path, wrap_angle def load_slam_dataset(data_dir: str) -> Dict: diff --git a/ch7_slam/example_scan_matching_visualization.py b/ch7_slam/example_scan_matching_visualization.py index 8c585ac..5b268ed 100644 --- a/ch7_slam/example_scan_matching_visualization.py +++ b/ch7_slam/example_scan_matching_visualization.py @@ -68,13 +68,26 @@ """ import argparse +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Ellipse -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) from core.slam.ndt import build_ndt_map, ndt_align, ndt_score from core.slam.scan_generation import generate_scan_with_occlusion from core.slam.scan_matching import ( diff --git a/ch7_slam/example_slam_frontend.py b/ch7_slam/example_slam_frontend.py index 03763a1..5c06425 100644 --- a/ch7_slam/example_slam_frontend.py +++ b/ch7_slam/example_slam_frontend.py @@ -16,13 +16,26 @@ """ import argparse +import sys from pathlib import Path from typing import Dict, List import matplotlib.pyplot as plt import numpy as np -from core.eval import plot_error_magnitude_time, plot_trajectory_2d, save_figure, show_figures_if_requested +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + plot_error_magnitude_time, + plot_trajectory_2d, + save_figure, + show_figures_if_requested, +) from core.slam import SlamFrontend2D, se2_relative FIGURE_NAME = "slam_frontend_demo" diff --git a/ch8_sensor_fusion/example_anchor_outage.py b/ch8_sensor_fusion/example_anchor_outage.py index 1364549..0aa581a 100644 --- a/ch8_sensor_fusion/example_anchor_outage.py +++ b/ch8_sensor_fusion/example_anchor_outage.py @@ -63,13 +63,26 @@ import argparse import copy +import sys from pathlib import Path import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.eval import ( + resolve_figs_dir, + save_animation, + save_figure, + show_figures_if_requested, +) from core.fusion import load_fusion_dataset, run_lc_fusion, run_tc_fusion -from core.eval import resolve_figs_dir, save_animation, save_figure, show_figures_if_requested FIGS_DIR = Path(__file__).parent / "figs" DEFAULT_DATA = "data/sim/ch8_fusion_2d_imu_uwb" diff --git a/ch8_sensor_fusion/example_calibration.py b/ch8_sensor_fusion/example_calibration.py index 694eabb..c9bd532 100644 --- a/ch8_sensor_fusion/example_calibration.py +++ b/ch8_sensor_fusion/example_calibration.py @@ -17,15 +17,24 @@ """ import argparse +import sys from pathlib import Path -from core.eval import save_figure, show_figures_if_requested +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from typing import Dict, Tuple import matplotlib.pyplot as plt import numpy as np from matplotlib.gridspec import GridSpec +from core.eval import save_figure, show_figures_if_requested + def wrap_angle_deg(angle_deg: float) -> float: """Wrap a degree-valued angle difference to [-180, 180]. diff --git a/ch8_sensor_fusion/example_comparison.py b/ch8_sensor_fusion/example_comparison.py index 440bfc5..885990b 100644 --- a/ch8_sensor_fusion/example_comparison.py +++ b/ch8_sensor_fusion/example_comparison.py @@ -12,6 +12,7 @@ import argparse import json +import sys from pathlib import Path from typing import Dict, Tuple @@ -19,6 +20,13 @@ import numpy as np from matplotlib.gridspec import GridSpec +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import ( compute_position_errors, compute_position_rmse, diff --git a/ch8_sensor_fusion/example_lc_fusion.py b/ch8_sensor_fusion/example_lc_fusion.py index d3bc393..12c783a 100644 --- a/ch8_sensor_fusion/example_lc_fusion.py +++ b/ch8_sensor_fusion/example_lc_fusion.py @@ -19,12 +19,20 @@ """ import argparse +import sys from pathlib import Path from typing import Dict import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import ( compute_position_errors, compute_position_rmse, diff --git a/ch8_sensor_fusion/example_observability.py b/ch8_sensor_fusion/example_observability.py index 497cb31..009e45a 100644 --- a/ch8_sensor_fusion/example_observability.py +++ b/ch8_sensor_fusion/example_observability.py @@ -19,6 +19,7 @@ """ import argparse +import sys from pathlib import Path from typing import Dict, Tuple @@ -26,6 +27,13 @@ import numpy as np from matplotlib.gridspec import GridSpec +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.estimators import ExtendedKalmanFilter from core.eval import save_figure, show_figures_if_requested diff --git a/ch8_sensor_fusion/example_robust_tuning.py b/ch8_sensor_fusion/example_robust_tuning.py index 3c7bb19..8735d32 100644 --- a/ch8_sensor_fusion/example_robust_tuning.py +++ b/ch8_sensor_fusion/example_robust_tuning.py @@ -37,6 +37,7 @@ """ import argparse +import sys from pathlib import Path from typing import Dict, List @@ -44,24 +45,31 @@ import numpy as np from matplotlib.gridspec import GridSpec +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.estimators import ExtendedKalmanFilter from core.eval import compute_rmse, save_figure, show_figures_if_requested from core.fusion import ( + cauchy_R_scale, chi_square_gate, huber_R_scale, - cauchy_R_scale, innovation, innovation_covariance, + load_fusion_dataset, mahalanobis_distance_squared, ) -from core.fusion import load_fusion_dataset from core.fusion.tc_models import ( - tc_process_model, tc_process_jacobian, + tc_process_model, tc_process_noise_covariance, - tc_uwb_measurement_model, tc_uwb_measurement_jacobian, + tc_uwb_measurement_model, ) -from core.estimators import ExtendedKalmanFilter def run_fusion_with_strategy( diff --git a/ch8_sensor_fusion/example_tc_fusion.py b/ch8_sensor_fusion/example_tc_fusion.py index fad2538..bbc09ed 100644 --- a/ch8_sensor_fusion/example_tc_fusion.py +++ b/ch8_sensor_fusion/example_tc_fusion.py @@ -15,12 +15,20 @@ """ import argparse +import sys from pathlib import Path from typing import Dict import matplotlib.pyplot as plt import numpy as np +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from core.eval import ( compute_position_errors, compute_position_rmse, diff --git a/ch8_sensor_fusion/example_temporal_calibration.py b/ch8_sensor_fusion/example_temporal_calibration.py index d5d5940..cafd509 100644 --- a/ch8_sensor_fusion/example_temporal_calibration.py +++ b/ch8_sensor_fusion/example_temporal_calibration.py @@ -35,6 +35,7 @@ """ import argparse +import sys from pathlib import Path from typing import Dict, List @@ -42,23 +43,29 @@ import numpy as np from matplotlib.gridspec import GridSpec +# `core` must come from this checkout. Running this file as a script puts +# its *chapter* directory on sys.path[0], not the repository root, so +# without this line `import core` silently resolves to whatever else is +# installed -- another clone, a stale editable install -- or fails outright +# on a fresh one. See issue #86. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from core.estimators import ExtendedKalmanFilter from core.eval import ( compute_position_rmse, compute_rmse, save_figure, show_figures_if_requested, ) -from core.fusion import StampedMeasurement, TimeSyncModel -from core.fusion import load_fusion_dataset +from core.fusion import StampedMeasurement, TimeSyncModel, load_fusion_dataset from core.fusion.tc_models import ( - tc_process_model, + interpolate_imu_measurements, tc_process_jacobian, + tc_process_model, tc_process_noise_covariance, - tc_uwb_measurement_model, tc_uwb_measurement_jacobian, - interpolate_imu_measurements, + tc_uwb_measurement_model, ) -from core.estimators import ExtendedKalmanFilter def run_fusion_with_time_sync( diff --git a/tests/test_examples_import_this_checkout.py b/tests/test_examples_import_this_checkout.py new file mode 100644 index 0000000..bf66138 --- /dev/null +++ b/tests/test_examples_import_this_checkout.py @@ -0,0 +1,152 @@ +"""An example must import the `core` sitting next to it, not one from elsewhere. + +`python ch4_rf_point_positioning/example_toa_positioning.py` puts the *chapter* +directory on ``sys.path[0]`` -- not the repository root -- so `import core` +falls through to whatever else is importable. On a fresh clone that is +``ModuleNotFoundError``. On a machine that has ever installed this package it +is worse and quieter: `core` resolves to **a different checkout**, and the +example runs to completion against someone else's source tree. + +Measured in this worktree before the fix, with a probe run as a script from the +chapter directory:: + + sys.path[0] = .../worktrees/wonderful-lamarr-aeb17f/ch4_rf_point_positioning + core -> C:/Users/qmohs/IPIN-Examples/core/__init__.py # main checkout + +and after it, the same probe resolves `core` inside the worktree. No error +either way; only the answer changes. + +**The repository already had the fix, in one file.** `example_classification` +in Chapter 5 has carried ``sys.path.insert(0, ...)`` for as long as it has +existed, and nine of the twelve generators in ``scripts/`` do the same. It was +never brought to the other 37 examples, which is what this file now holds. + +The convention it enforces is deliberately about *order*, not about presence: +the bootstrap has to run **before** the first `core` import, because after it +the import has already resolved. A file that has both, in the wrong order, is +the failure this test exists to name. + +Note the sibling guard `tests/docs/test_documented_commands_use_module_form.py` +covers the same hazard from the documentation side, and neither subsumes the +other: that one keeps the READMEs telling readers to type ``python -m``, this +one makes the other spelling work anyway. A reader who ignores the docs, an IDE +"run this file" button, and a copy-pasted path all reach the script form. + +Author: Li-Ta Hsu +""" + +import ast +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent + +#: Examples that legitimately never import `core` and so need no bootstrap. +#: Empty today; an entry here should say why in a comment. +KNOWN_WITHOUT_CORE: set = set() + + +def _examples(): + return sorted(REPO_ROOT.glob("ch*/example_*.py")) + + +def _first_core_import(tree): + """Line number of the first module-level `core` import, or None.""" + for node in tree.body: + if isinstance(node, ast.ImportFrom): + module = node.module + elif isinstance(node, ast.Import): + module = node.names[0].name + else: + continue + if module and module.split(".")[0] == "core": + return node.lineno + return None + + +def _bootstrap_lines(tree): + """Line numbers of module-level `sys.path.insert(...)` / `append` calls.""" + found = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + func = node.func + if not isinstance(func, ast.Attribute) or func.attr not in {"insert", "append"}: + continue + target = func.value + if ( + isinstance(target, ast.Attribute) + and target.attr == "path" + and isinstance(target.value, ast.Name) + and target.value.id == "sys" + ): + found.append(node.lineno) + return sorted(found) + + +@pytest.mark.parametrize("example", _examples(), ids=lambda p: p.name) +def test_example_puts_the_repo_root_on_sys_path_before_importing_core(example): + relative = example.relative_to(REPO_ROOT).as_posix() + tree = ast.parse(example.read_text(encoding="utf-8")) + + core_line = _first_core_import(tree) + if core_line is None: + assert relative in KNOWN_WITHOUT_CORE or True, relative + pytest.skip(f"{relative} imports no core") + + bootstraps = _bootstrap_lines(tree) + assert bootstraps, ( + f"{relative} imports `core` at line {core_line} without putting the " + f"repository root on sys.path first. Run as a script it will import a " + f"`core` from somewhere else -- another clone, a stale editable " + f"install -- or fail outright on a fresh one. Add, above that import:\n" + f" sys.path.insert(0, str(Path(__file__).resolve().parent.parent))" + ) + assert bootstraps[0] < core_line, ( + f"{relative} adjusts sys.path at line {bootstraps[0]}, which is after " + f"its first `core` import at line {core_line}. By then the import has " + f"already resolved, so the bootstrap changes nothing. Move it above." + ) + + +def test_the_check_reads_the_shapes_it_has_to_distinguish(): + """The parser is the risky half here, so pin what it must tell apart. + + Two of these are the mistakes an earlier version of the *sweep* that wrote + these bootstraps actually made, and pyflakes rather than review caught both: + a name imported inside a function is not available at module level, and a + line of docstring prose beginning "from range measurements ..." is not an + import. A checker that reads Python by line prefix cannot see either. + """ + ordered = ast.parse( + "import sys\n" + "from pathlib import Path\n" + "sys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n" + "from core.eval import save_figure\n" + ) + assert _first_core_import(ordered) == 4 + assert _bootstrap_lines(ordered) == [3] + + # Bootstrap after the import: present, useless, and must not pass. + reversed_order = ast.parse( + "import sys\n" + "from core.eval import save_figure\n" + "sys.path.insert(0, '.')\n" + ) + assert _bootstrap_lines(reversed_order)[0] > _first_core_import(reversed_order) + + # Prose that begins a line with "from " is not an import. + prose = ast.parse( + '"""Estimate position\n\nfrom range measurements, following Chapter 3.\n"""\n' + "from core.eval import save_figure\n" + ) + assert _first_core_import(prose) == 5 + + # `core` as a substring of another package is not `core`. + lookalike = ast.parse("from corelib import thing\nimport coreutils\n") + assert _first_core_import(lookalike) is None + + # An import inside a function is not module level. + nested = ast.parse("def main():\n from core.eval import save_figure\n") + assert _first_core_import(nested) is None diff --git a/tests/test_lint_debt_only_shrinks.py b/tests/test_lint_debt_only_shrinks.py index d64dbd5..6e4142e 100644 --- a/tests/test_lint_debt_only_shrinks.py +++ b/tests/test_lint_debt_only_shrinks.py @@ -70,7 +70,7 @@ "UP006": 386, "UP045": 178, "UP035": 123, - "I001": 113, + "I001": 84, "W291": 70, "B905": 41, "UP007": 38,