From 7323e57d51fe8538afdb1048ca731a837fa47cff Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 00:29:07 +0800
Subject: [PATCH 1/8] chore: cleanup gitignore
---
.gitignore | 197 ++++-------------------------------------------------
1 file changed, 14 insertions(+), 183 deletions(-)
diff --git a/.gitignore b/.gitignore
index b40d36f..69d210c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,187 +1,18 @@
-
-# Automatically generated directories and files
-output
-cache
-
-# tangram frontend plugin builds (example_plugins/tangram/*)
+# generated frontend artifacts
node_modules/
dist-frontend/
-bluesky/resources/grib
-bluesky/resources/netcdf
-
-.vscode
-# Byte-compiled / optimized / DLL files
+# python
__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# UV
-# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-#uv.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# zensical documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-# Ruff stuff:
-.ruff_cache/
-
-# PyPI configuration file
-.pypirc
+/.pytest_cache/
+/.ruff_cache/
+/.venv/
+/dist/
+
+# zensical
+/.cache/
+/site/
+
+# misc
+.vscode/
+.DS_Store
From 5edc341ba8c40d2b16567a0e58eb784b65722b60 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 00:49:30 +0800
Subject: [PATCH 2/8] chore(deps): upgrade ruff to 0.16
slim down on the lints following
https://github.com/open-aviation/tangram/commit/d506079d9760f1f4d0984f24ad112b002007e86f
---
.../src/minisky_example/__init__.py | 1 -
.../src/minisky_tangram/__init__.py | 3 +-
packages/minisky/minisky/__init__.py | 4 +-
packages/minisky/minisky/cli.py | 2 +-
.../minisky/minisky/core/trafficarrays.py | 3 +-
packages/minisky/minisky/core/varexplorer.py | 4 +-
packages/minisky/minisky/plugin/plugin.py | 10 ++---
packages/minisky/minisky/runtime.py | 13 +++---
packages/minisky/minisky/server.py | 6 +--
.../minisky/minisky/simulation/console.py | 7 ++-
packages/minisky/minisky/stack/__init__.py | 6 +--
packages/minisky/minisky/stack/argparser.py | 2 -
packages/minisky/minisky/tools/__init__.py | 7 ++-
packages/minisky/minisky/tools/aero.py | 12 ++---
.../minisky/minisky/traffic/asas/__init__.py | 6 +--
packages/minisky/minisky/traffic/asas/mvp.py | 3 +-
packages/minisky/minisky/traffic/autopilot.py | 2 -
.../minisky/minisky/traffic/conditional.py | 5 +--
.../minisky/traffic/performance/__init__.py | 4 +-
.../minisky/traffic/performance/perfoap.py | 18 ++++----
packages/minisky/minisky/traffic/route.py | 27 ++++++------
packages/minisky/minisky/traffic/traffic.py | 15 ++++---
.../minisky/minisky/traffic/trafficgroups.py | 4 +-
packages/minisky/minisky/traffic/trails.py | 7 ---
packages/minisky/minisky/traffic/wind.py | 8 +---
pyproject.toml | 28 +-----------
tests/conftest.py | 2 +-
tests/integration/test_conflict.py | 2 +-
tests/integration/test_navdata.py | 2 +-
tests/integration/test_plugin.py | 3 +-
tests/integration/test_route_autopilot.py | 11 ++++-
tests/integration/test_scenario.py | 2 +-
tests/integration/test_stack.py | 2 +-
tests/integration/test_streaming.py | 2 +-
tests/integration/test_tangram_bridge.py | 3 +-
tests/integration/test_traffic.py | 8 ++--
tests/test_api.py | 1 -
tests/unit/test_aero.py | 5 +--
tests/unit/test_areafilter.py | 3 +-
tests/unit/test_convert.py | 1 -
tests/unit/test_detection.py | 1 -
tests/unit/test_geo.py | 1 -
tests/unit/test_phase.py | 1 -
tests/unit/test_tangram_plugin.py | 1 -
uv.lock | 44 +++++++++----------
45 files changed, 132 insertions(+), 170 deletions(-)
diff --git a/packages/minisky-example/src/minisky_example/__init__.py b/packages/minisky-example/src/minisky_example/__init__.py
index 093d803..3070418 100644
--- a/packages/minisky-example/src/minisky_example/__init__.py
+++ b/packages/minisky-example/src/minisky_example/__init__.py
@@ -5,7 +5,6 @@
from random import Random
import numpy as np
-
from minisky import plugin as plugin_api
diff --git a/packages/minisky-tangram/src/minisky_tangram/__init__.py b/packages/minisky-tangram/src/minisky_tangram/__init__.py
index 797a51a..31f9d68 100644
--- a/packages/minisky-tangram/src/minisky_tangram/__init__.py
+++ b/packages/minisky-tangram/src/minisky_tangram/__init__.py
@@ -49,12 +49,11 @@
from datetime import UTC, datetime
from typing import Any, TypedDict, cast
-from pydantic import BaseModel, ConfigDict
-
from minisky import plugin as plugin_api
from minisky.simulation import SimulationState
from minisky.streaming import Snapshot
from minisky.tools.aero import fpm, ft, kts
+from pydantic import BaseModel, ConfigDict
# --8<-- [start:configuration]
diff --git a/packages/minisky/minisky/__init__.py b/packages/minisky/minisky/__init__.py
index 004f1c2..8289e7b 100644
--- a/packages/minisky/minisky/__init__.py
+++ b/packages/minisky/minisky/__init__.py
@@ -23,9 +23,9 @@
"BS_CMDERR",
"BS_FUNERR",
"BS_OK",
- "default_user_config_dir",
- "default_user_config_toml_path",
"MiniSky",
"MiniSkyConfig",
"SimulationState",
+ "default_user_config_dir",
+ "default_user_config_toml_path",
)
diff --git a/packages/minisky/minisky/cli.py b/packages/minisky/minisky/cli.py
index e19bc23..5da21b9 100644
--- a/packages/minisky/minisky/cli.py
+++ b/packages/minisky/minisky/cli.py
@@ -145,7 +145,7 @@ def console_cmd(
os.system("clear")
continue
- if cmd.startswith("/load ") or cmd.startswith("load "):
+ if cmd.startswith(("/load ", "load ")):
file_path = cmd.split(" ", maxsplit=1)[1]
if os.path.isfile(file_path):
diff --git a/packages/minisky/minisky/core/trafficarrays.py b/packages/minisky/minisky/core/trafficarrays.py
index 92dc998..04d8220 100644
--- a/packages/minisky/minisky/core/trafficarrays.py
+++ b/packages/minisky/minisky/core/trafficarrays.py
@@ -57,7 +57,7 @@ class _ComponentSlot:
def current(self) -> TrafficArrays:
component = getattr(self.traffic, self.attribute)
if not isinstance(component, self.base):
- raise RuntimeError(f"replaceable slot {self.attribute} has an invalid component")
+ raise TypeError(f"replaceable slot {self.attribute} has an invalid component")
return component
def bind(self, callback: Callable[..., Any]) -> Callable[..., Any]:
@@ -225,7 +225,6 @@ def __init__(self, parent: TrafficArrays) -> None:
def __enter__(self) -> None:
"""No-op: the attribute snapshot is already taken in __init__."""
- pass
def __exit__(self, exc_type, exc_value, tb) -> None:
"""Register all attributes created inside the with-block as traffic arrays."""
diff --git a/packages/minisky/minisky/core/varexplorer.py b/packages/minisky/minisky/core/varexplorer.py
index d965a37..ef24e33 100644
--- a/packages/minisky/minisky/core/varexplorer.py
+++ b/packages/minisky/minisky/core/varexplorer.py
@@ -135,8 +135,8 @@ def findvar(self, varname: str) -> Variable | None:
for objname, objset in self.varlist.items():
if objset[1] is not None and name in objset[1]:
return Variable(objset[0], objname, name, index)
- except Exception:
- pass
+ except (AttributeError, IndexError, KeyError, TypeError, ValueError):
+ return None
return None
diff --git a/packages/minisky/minisky/plugin/plugin.py b/packages/minisky/minisky/plugin/plugin.py
index b30e756..e736dd2 100644
--- a/packages/minisky/minisky/plugin/plugin.py
+++ b/packages/minisky/minisky/plugin/plugin.py
@@ -384,7 +384,7 @@ async def _load(self, plugin: _PluginRecord) -> CommandReply:
if entered and lifespan is not None:
try:
await lifespan.__aexit__(type(exc), exc, exc.__traceback__)
- except BaseException as cleanup_error:
+ except BaseException as cleanup_error: # ruff: ignore[BLE001] lifespan cleanup is arbitrary
traceback.print_exception(cleanup_error)
if not isinstance(exc, Exception):
raise
@@ -604,7 +604,7 @@ def _run_hooks(self, phase: HookName) -> None:
hook.callback(dt=elapsed)
else:
hook.callback()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] plugin hooks are arbitrary
hook.enabled = False
traceback.print_exception(exc)
self.console.echo(
@@ -624,13 +624,13 @@ async def aclose(self) -> None:
plugin.runtime._revoke()
try:
self._remove(plugin)
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate removal failures
errors.append(exc)
if plugin.lifespan is not None:
try:
await plugin.lifespan.__aexit__(None, None, None)
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] plugin lifespan is arbitrary
errors.append(exc)
self._clear(plugin)
@@ -656,7 +656,7 @@ def _remove(self, plugin: _PluginRecord) -> None:
for cleanup in cleanups:
try:
cleanup()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate cleanup failures
errors.append(exc)
if errors:
raise ExceptionGroup(f"Plugin {plugin.plugin_name} removal failed", errors)
diff --git a/packages/minisky/minisky/runtime.py b/packages/minisky/minisky/runtime.py
index a351acf..856a8e0 100644
--- a/packages/minisky/minisky/runtime.py
+++ b/packages/minisky/minisky/runtime.py
@@ -3,6 +3,7 @@
from __future__ import annotations
from random import Random
+from typing import Self
import numpy as np
@@ -137,7 +138,7 @@ def close(self) -> None:
for cleanup in (self.runner.shutdown, self.streaming.close, self.plugins.close):
try:
cleanup()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate resource failures
errors.append(exc)
self._closed = True
@@ -152,21 +153,21 @@ async def aclose(self) -> None:
try:
await self.commands.aclose()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate resource failures
errors.append(exc)
try:
await self.plugins.aclose()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate resource failures
errors.append(exc)
try:
self.streaming.close()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate resource failures
errors.append(exc)
self._closed = True
self._raise_errors("MiniSky shutdown failed", errors)
- def __enter__(self) -> MiniSky:
+ def __enter__(self) -> Self:
"""Enter a synchronous runtime lifecycle context."""
return self
@@ -174,7 +175,7 @@ def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> No
"""Close the runtime when leaving a synchronous context."""
self.close()
- async def __aenter__(self) -> MiniSky:
+ async def __aenter__(self) -> Self:
"""Enter an asynchronous runtime lifecycle context."""
return self
diff --git a/packages/minisky/minisky/server.py b/packages/minisky/minisky/server.py
index 71e5def..a71a9bc 100644
--- a/packages/minisky/minisky/server.py
+++ b/packages/minisky/minisky/server.py
@@ -68,11 +68,11 @@ async def lifespan(app: FastAPI):
with suppress(asyncio.CancelledError):
try:
await runner_task
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate server cleanup failures
errors.append(exc)
try:
await runtime.aclose()
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] aggregate server cleanup failures
errors.append(exc)
if len(errors) == 1:
raise errors[0]
@@ -245,7 +245,7 @@ def upload_form() -> Response:
return Response(content=content, media_type="text/html")
-async def scn(runtime: Runtime, file: UploadFile = File(...)) -> dict[str, str]:
+async def scn(runtime: Runtime, file: Annotated[UploadFile, File()]) -> dict[str, str]:
"""Load an uploaded scenario file into the running simulation."""
runtime.console.event.clear()
contents = await file.read()
diff --git a/packages/minisky/minisky/simulation/console.py b/packages/minisky/minisky/simulation/console.py
index 0a97f4e..1b67900 100644
--- a/packages/minisky/minisky/simulation/console.py
+++ b/packages/minisky/minisky/simulation/console.py
@@ -7,6 +7,7 @@
import sys
import traceback
from collections.abc import Callable
+from typing import Self
from colorama import Fore, Style
@@ -27,7 +28,7 @@ def close(self) -> None:
self._closed = True
self._console._unsubscribe(self._token)
- def __enter__(self) -> ConsoleSubscription:
+ def __enter__(self) -> Self:
return self
def __exit__(self, *_args: object) -> None:
@@ -95,7 +96,7 @@ def echo(self, text: str = "", flag: int = 0) -> None:
for token, callback in tuple(self._subscribers.items()):
try:
callback(text)
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] subscribers are arbitrary
self._subscribers.pop(token, None)
traceback.print_exception(exc)
@@ -115,11 +116,9 @@ def getviewctr(self) -> tuple[float, float]:
def addnavwpt(self, name: str, lat: float, lon: float) -> None:
"""Add a waypoint marker. Stub for non-GUI mode."""
- pass
def removenavwpt(self, name: str) -> None:
"""Remove a waypoint marker. Stub for non-GUI mode."""
- pass
def read_output_buffer(self) -> str:
"""Return and clear buffered console output."""
diff --git a/packages/minisky/minisky/stack/__init__.py b/packages/minisky/minisky/stack/__init__.py
index a22074a..89ed1ad 100644
--- a/packages/minisky/minisky/stack/__init__.py
+++ b/packages/minisky/minisky/stack/__init__.py
@@ -38,7 +38,7 @@
import numpy as np
-from minisky.plugin.plugin_decorators import command
+from minisky.plugin.plugin_decorators import command as command
from minisky.stack import argparser, commands
from minisky.stack.argparser import ArgumentError, Parameter, String, Time, Txt, getnextarg
@@ -514,7 +514,7 @@ def process(self) -> bool:
header = "" if not argstring else exc.args[0] if exc.args else "Argument error."
self.console.echo(f"{header}\nUsage:\n{cmdobj.brieftext()}")
continue
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] commands are arbitrary callbacks
self._echo_command_exception(cmdu, argstring, exc)
continue
@@ -549,7 +549,7 @@ def _finish_pending_command(self) -> bool:
result = pending.task.result()
except asyncio.CancelledError:
return True
- except Exception as exc:
+ except Exception as exc: # ruff: ignore[BLE001] commands are arbitrary callbacks
self._echo_command_exception(pending.name, pending.argstring, exc)
else:
self._echo_command_result(pending.command, pending.argstring, result)
diff --git a/packages/minisky/minisky/stack/argparser.py b/packages/minisky/minisky/stack/argparser.py
index 0a1af71..4b59d7b 100644
--- a/packages/minisky/minisky/stack/argparser.py
+++ b/packages/minisky/minisky/stack/argparser.py
@@ -196,8 +196,6 @@ def canwrap(param: inspect.Parameter) -> bool:
class ArgumentError(Exception):
"""This error is raised when stack argument parsing fails."""
- pass
-
class Parser:
"""Base implementation of argument parsers
diff --git a/packages/minisky/minisky/tools/__init__.py b/packages/minisky/minisky/tools/__init__.py
index 059db78..34bf525 100644
--- a/packages/minisky/minisky/tools/__init__.py
+++ b/packages/minisky/minisky/tools/__init__.py
@@ -7,4 +7,9 @@
parsing (position).
"""
-from . import aero, areafilter, convert, geo, navdata, position
+from . import aero as aero
+from . import areafilter as areafilter
+from . import convert as convert
+from . import geo as geo
+from . import navdata as navdata
+from . import position as position
diff --git a/packages/minisky/minisky/tools/aero.py b/packages/minisky/minisky/tools/aero.py
index 409dc1e..584baae 100644
--- a/packages/minisky/minisky/tools/aero.py
+++ b/packages/minisky/minisky/tools/aero.py
@@ -500,7 +500,7 @@ def pressure(h: float) -> float: # h [m]
Returns:
Pressure [Pa].
"""
- p, r, T = atmos(h)
+ p, _r, _T = atmos(h)
return p
@@ -513,7 +513,7 @@ def density(h: float) -> float: # air density at given altitude h [m]
Returns:
Density [kg/m3].
"""
- p, r, T = atmos(h)
+ _p, r, _T = atmos(h)
return r
@@ -613,7 +613,7 @@ def cas2tas(cas: float, h: float) -> float:
Returns:
True airspeed [m/s].
"""
- p, rho, T = atmos(h)
+ p, rho, _T = atmos(h)
qdyn = p0 * ((1.0 + rho0 * cas * cas / (7.0 * p0)) ** 3.5 - 1.0)
tas = np.sqrt(7.0 * p / rho * ((1.0 + qdyn / p) ** (2.0 / 7.0) - 1.0))
tas = -1 * tas if cas < 0 else tas
@@ -634,7 +634,7 @@ def tas2cas(tas: float, h: float) -> float:
Returns:
Calibrated airspeed [m/s].
"""
- p, rho, T = atmos(h)
+ p, rho, _T = atmos(h)
qdyn = p * ((1.0 + rho * tas * tas / (7.0 * p)) ** 3.5 - 1.0)
cas = np.sqrt(7.0 * p0 / rho0 * ((qdyn / p0 + 1.0) ** (2.0 / 7.0) - 1.0))
cas = -1 * cas if tas < 0 else cas
@@ -729,7 +729,7 @@ def metres_to_feet_rounded(metres: float) -> int:
Converts metres to feet.
Returns feet as rounded integer.
"""
- return int(round(metres / ft))
+ return round(metres / ft)
def metric_spd_to_knots_rounded(speed: float) -> int:
@@ -737,4 +737,4 @@ def metric_spd_to_knots_rounded(speed: float) -> int:
Converts speed in m/s to knots.
Returns knots as rounded integer.
"""
- return int(round(speed / kts))
+ return round(speed / kts)
diff --git a/packages/minisky/minisky/traffic/asas/__init__.py b/packages/minisky/minisky/traffic/asas/__init__.py
index 9bde372..92f465e 100644
--- a/packages/minisky/minisky/traffic/asas/__init__.py
+++ b/packages/minisky/minisky/traffic/asas/__init__.py
@@ -16,8 +16,8 @@
# isort: off
# Import order matters: MVP subclasses ConflictResolution, so resolution must
# be importable before mvp to avoid a partially-initialised circular import.
-from .detection import ConflictDetection
-from .resolution import ConflictResolution
-from .mvp import MVP
+from .detection import ConflictDetection as ConflictDetection
+from .resolution import ConflictResolution as ConflictResolution
+from .mvp import MVP as MVP
# isort: on
diff --git a/packages/minisky/minisky/traffic/asas/mvp.py b/packages/minisky/minisky/traffic/asas/mvp.py
index e82d0ed..5dfc934 100644
--- a/packages/minisky/minisky/traffic/asas/mvp.py
+++ b/packages/minisky/minisky/traffic/asas/mvp.py
@@ -330,8 +330,7 @@ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
# Because ADSB is ON, this is done for each aircraft separately
if idx1 > -1 and idx2 > -1:
dv_mvp, tsolV = self.MVP(ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2)
- if tsolV < timesolveV[idx1]:
- timesolveV[idx1] = tsolV
+ timesolveV[idx1] = min(timesolveV[idx1], tsolV)
# Use priority rules if activated
if self.swprio:
diff --git a/packages/minisky/minisky/traffic/autopilot.py b/packages/minisky/minisky/traffic/autopilot.py
index f2f0f2d..36b61da 100644
--- a/packages/minisky/minisky/traffic/autopilot.py
+++ b/packages/minisky/minisky/traffic/autopilot.py
@@ -585,7 +585,6 @@ def update(self) -> None:
self.traffic.alt,
self.traffic.casmach_threshold,
)
- #
dxspdconchg = distaccel(self.traffic.tas, nexttas, self.traffic.perf.axmax)
qdrturn, dist2turn = geo.qdrdist(
@@ -769,7 +768,6 @@ def ComputeVNAV(self, idx: int, toalt: Any, xtoalt: Any, torta: Any, xtorta: Any
# - Descend at the latest when necessary for next altitude constraint
# which can be many waypoints beyond current actual waypoint
epsalt = 2.0 * ft # deadzone
- #
if self.traffic.alt[idx] > toalt + epsalt:
# Stop potential current climb (e.g. due to not making it to previous altco)
# then stop immediately, as in: do not make it worse.
diff --git a/packages/minisky/minisky/traffic/conditional.py b/packages/minisky/minisky/traffic/conditional.py
index c74323a..1724b76 100644
--- a/packages/minisky/minisky/traffic/conditional.py
+++ b/packages/minisky/minisky/traffic/conditional.py
@@ -109,7 +109,7 @@ def update(self) -> None:
) # Invalid number which never triggers anything is extremely large
for j in range(self.ncond):
if self.condtype[j] == postype:
- qdr, dist = qdrdist(
+ _qdr, dist = qdrdist(
self.traffic.lat[acidxlst[j]],
self.traffic.lon[acidxlst[j]],
self.posdata[j][0],
@@ -213,7 +213,7 @@ def atdistcmd(self, acidx: int, lat: float, lon: float, targdist: float, cmdtxt:
Returns:
bool: True (the condition is always added).
"""
- qdr, actdist = qdrdist(self.traffic.lat[acidx], self.traffic.lon[acidx], lat, lon)
+ _qdr, actdist = qdrdist(self.traffic.lat[acidx], self.traffic.lon[acidx], lat, lon)
self.addcondition(acidx, postype, targdist, actdist, cmdtxt, (lat, lon))
return True
@@ -255,7 +255,6 @@ def addcondition(
self.ncond = self.ncond + 1
# print("addcondition: self.ncond",self.ncond)
- return
def renameac(self, oldid: str, newid: str) -> None:
"""Update stored callsigns after an aircraft has been renamed.
diff --git a/packages/minisky/minisky/traffic/performance/__init__.py b/packages/minisky/minisky/traffic/performance/__init__.py
index 47b8036..b2bbba3 100644
--- a/packages/minisky/minisky/traffic/performance/__init__.py
+++ b/packages/minisky/minisky/traffic/performance/__init__.py
@@ -14,5 +14,5 @@
The active performance model instance is [`runtime.traffic.perf`][minisky.traffic.performance.perfoap.OpenAP].
"""
-import minisky.traffic.performance.coeff
-import minisky.traffic.performance.phase
+from . import coeff as coeff
+from . import phase as phase
diff --git a/packages/minisky/minisky/traffic/performance/perfoap.py b/packages/minisky/minisky/traffic/performance/perfoap.py
index 3b23285..ff91318 100644
--- a/packages/minisky/minisky/traffic/performance/perfoap.py
+++ b/packages/minisky/minisky/traffic/performance/perfoap.py
@@ -168,7 +168,7 @@ def create(self, n: int = 1) -> None:
# populate fuel flow model
es = self.coeff.acs_fixwing[actype]["engines"]
- e = es[list(es.keys())[0]]
+ e = es[next(iter(es.keys()))]
coeff_a, coeff_b, coeff_c = thrust.compute_eng_ff_coeff(
e["ff_idl"], e["ff_app"], e["ff_co"], e["ff_to"]
)
@@ -537,11 +537,13 @@ def show_performance(self, acid: int) -> tuple:
"""
return (
True,
- f"Flight phase: {ph.readable_phase(self.phase[acid])}\n"
- f"Thrust: {self.thrust[acid] / 1000:.0f} kN\n"
- f"Drag: {self.drag[acid] / 1000:.0f} kN\n"
- f"Fuel flow: {self.fuelflow[acid]:.2f} kg/s\n"
- f"Speed envelope: [{self.vmin[acid] / kts:.0f}, {self.vmax[acid] / kts:.0f}] kts\n"
- f"Vertical speed envelope: [{self.vsmin[acid] / fpm:.0f}, {self.vsmax[acid] / fpm:.0f}] fpm\n"
- f"Ceiling: {self.hmax[acid] / ft:.0f} ft",
+ (
+ f"Flight phase: {ph.readable_phase(self.phase[acid])}\n"
+ f"Thrust: {self.thrust[acid] / 1000:.0f} kN\n"
+ f"Drag: {self.drag[acid] / 1000:.0f} kN\n"
+ f"Fuel flow: {self.fuelflow[acid]:.2f} kg/s\n"
+ f"Speed envelope: [{self.vmin[acid] / kts:.0f}, {self.vmax[acid] / kts:.0f}] kts\n"
+ f"Vertical speed envelope: [{self.vsmin[acid] / fpm:.0f}, {self.vsmax[acid] / fpm:.0f}] fpm\n"
+ f"Ceiling: {self.hmax[acid] / ft:.0f} ft"
+ ),
)
diff --git a/packages/minisky/minisky/traffic/route.py b/packages/minisky/minisky/traffic/route.py
index c7033ad..c6400aa 100644
--- a/packages/minisky/minisky/traffic/route.py
+++ b/packages/minisky/minisky/traffic/route.py
@@ -532,7 +532,6 @@ def runactwpstack(self) -> None:
self.traffic.stack_command(cmdline)
# debug
# stack.stack("ECHO "+self.acid+" AT "+self.wpname[self.iactwp]+" command issued:"+cmdline)
- return
def insertcalcwp(self, i: int, name: str) -> None:
"""Insert an empty calculated waypoint (T/C, T/D) at location i."""
@@ -599,7 +598,7 @@ def calcfp(self) -> None:
# Calculate lateral leg data
# LNAV: Calculate leg distances and directions
- for i in range(0, n_wpt - 1):
+ for i in range(n_wpt - 1):
qdr, dist = geo.qdrdist(
self.wplat[i], self.wplon[i], self.wplat[i + 1], self.wplon[i + 1]
)
@@ -757,7 +756,7 @@ def getnextqdr(self):
"""
# get qdr for next leg
if -1 < self.iactwp < len(self.wpname) - 1:
- nextqdr, dist = geo.qdrdist(
+ nextqdr, _dist = geo.qdrdist(
self.wplat[self.iactwp],
self.wplon[self.iactwp],
self.wplat[self.iactwp + 1],
@@ -925,7 +924,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
acrte.turnrad = -999
else:
acrte.turnrad = float(args[1] / ft * nm) # arg was originally parsed as wpalt
- except Exception:
+ except (TypeError, ValueError):
return False, "Error in processing value of turn radius"
# Switch flyturn automatically when this is set
@@ -942,7 +941,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
acrte.turnspd = (
args[1] * kts / ft
) # [m/s] Arg was wpalt Keep it as IAS/CAS orig in kts, now in m/s
- except Exception:
+ except (TypeError, ValueError):
return False, "Error in processing value of turn speed"
# Switch flyturn automatically when this is set
@@ -955,7 +954,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
acrte.turnhdgr = -999
else:
acrte.turnhdgr = args[1] / ft # [deg/s] turn rate
- except Exception:
+ except (TypeError, ValueError):
return False, "Error in processing value of turn heading rate"
# Switch flyturn automatically when this is set
@@ -1073,7 +1072,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
# Try to get it from the database
try:
rwyhdg = traffic.navigation.rwythresholds[aptid][rwyid][2]
- except Exception:
+ except (IndexError, KeyError, TypeError):
rwydir = rwyid.replace("L", "").replace("R", "").replace("C", "")
try:
rwyhdg = float(rwydir) * 10.0
@@ -1264,11 +1263,11 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
txt += "-----"
elif acrte.wpalt[wpidx] > 4500 * ft:
- fl = int(round(acrte.wpalt[wpidx] / (100.0 * ft)))
+ fl = round(acrte.wpalt[wpidx] / (100.0 * ft))
txt += "FL" + str(fl)
else:
- txt += str(int(round(acrte.wpalt[wpidx] / ft)))
+ txt += str(round(acrte.wpalt[wpidx] / ft))
if swspd:
txt += "/"
@@ -1278,7 +1277,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
if acrte.wpspd[wpidx] < 0:
txt += "---"
else:
- txt += str(int(round(acrte.wpspd[wpidx] / kts)))
+ txt += str(round(acrte.wpspd[wpidx] / kts))
# Type
if swalt and swspd:
@@ -1382,7 +1381,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
else:
# This command does not need an acid or it is already first argument
acrte.wpstack[wpidx].append(" ".join(args[1:]))
- except Exception:
+ except (AttributeError, IndexError, KeyError, TypeError):
return (
False,
"Stacked command " + cmd + " unknown or syntax error",
@@ -1597,17 +1596,17 @@ def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> tuple | None:
txt += "-----/"
elif acrte.wpalt[i] > 4500 * ft:
- fl = int(round(acrte.wpalt[i] / (100.0 * ft)))
+ fl = round(acrte.wpalt[i] / (100.0 * ft))
txt += "FL" + str(fl) + "/"
else:
- txt += str(int(round(acrte.wpalt[i] / ft))) + "/"
+ txt += str(round(acrte.wpalt[i] / ft)) + "/"
# Speed
if acrte.wpspd[i] < 0.0:
txt += "---"
elif acrte.wpspd[i] > 2.0:
- txt += str(int(round(acrte.wpspd[i] / kts)))
+ txt += str(round(acrte.wpspd[i] / kts))
else:
txt += "M" + str(acrte.wpspd[i])
diff --git a/packages/minisky/minisky/traffic/traffic.py b/packages/minisky/minisky/traffic/traffic.py
index f18fead..61d880f 100644
--- a/packages/minisky/minisky/traffic/traffic.py
+++ b/packages/minisky/minisky/traffic/traffic.py
@@ -256,9 +256,11 @@ def casmachthr(self, threshold: float | None = None) -> tuple[bool, str]:
if threshold is None:
return (
True,
- "CASMACHTHR: The current CAS/Mach threshold is "
- f"{self.casmach_threshold} m/s "
- f"({self.casmach_threshold / kts} kts)",
+ (
+ "CASMACHTHR: The current CAS/Mach threshold is "
+ f"{self.casmach_threshold} m/s "
+ f"({self.casmach_threshold / kts} kts)"
+ ),
)
self.casmach_threshold = threshold
@@ -873,7 +875,6 @@ def engchange(self, acid: int, engid: str) -> None:
engid: New engine type identifier.
"""
self.perf.engchange(acid, engid) # type: ignore[attr-defined]
- return
def move(
self,
@@ -1037,7 +1038,7 @@ def position_by_name(self, name: str) -> tuple[bool, str]:
lines += (
f"{aptname} is a {airport_size} airport in {country_name} ({country_code}):\n"
f"Position: {latlon2txt(aptlat, aptlon)}\n"
- f"Elevation: {int(round(aptelev / ft))} ft \n"
+ f"Elevation: {round(aptelev / ft)} ft \n"
)
if self.navigation.aptid[idx_airport] in self.navigation.rwythresholds:
@@ -1149,8 +1150,8 @@ def settrans(self, alt: float = -999.0) -> bool | tuple[bool, str]:
return False, "Transition level needs to be ft/FL and larger than zero"
# In case no value is given, show it
- tlvl = int(round(self.translvl / ft))
- return True, f"Transition level = {tlvl}/FL{int(round(tlvl / 100.0))}"
+ tlvl = round(self.translvl / ft)
+ return True, f"Transition level = {tlvl}/FL{round(tlvl / 100.0)}"
def setbanklim(self, idx: int, bankangle: float | None = None) -> bool | tuple[bool, str]:
"""Set or show the bank angle limit for a given aircraft.
diff --git a/packages/minisky/minisky/traffic/trafficgroups.py b/packages/minisky/minisky/traffic/trafficgroups.py
index b696ad0..c174684 100644
--- a/packages/minisky/minisky/traffic/trafficgroups.py
+++ b/packages/minisky/minisky/traffic/trafficgroups.py
@@ -12,7 +12,7 @@
from __future__ import annotations
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Self
import numpy as np
@@ -32,7 +32,7 @@ class GroupArray(np.ndarray):
"""
# Similar to normal numpy arrays, but with the attribute of a groupname
- def __new__(cls, *args, groupname: str = "", **kwargs) -> GroupArray:
+ def __new__(cls, *args, groupname: str = "", **kwargs) -> Self:
ret = np.array(*args, **kwargs).view(cls)
ret.groupname = groupname
return ret
diff --git a/packages/minisky/minisky/traffic/trails.py b/packages/minisky/minisky/traffic/trails.py
index aa1f97e..2ffb933 100644
--- a/packages/minisky/minisky/traffic/trails.py
+++ b/packages/minisky/minisky/traffic/trails.py
@@ -102,8 +102,6 @@ def __init__(
self.clearnew()
- return
-
def new_implementation(self, implementation: Callable[..., TrafficArrays]) -> TrafficArrays:
"""Construct a replacement with this runtime's traffic and simulation."""
return implementation(self.traffic, self._get_simulation)
@@ -215,7 +213,6 @@ def buffer(self) -> None:
self.bgacid = self.bgacid + self.acid
self.clearfg() # Clear foreground trails
- return
def clearnew(self) -> None:
"""Clear the pipeline of new line segments used for the QtGL GUI."""
@@ -233,7 +230,6 @@ def clearfg(self) -> None: # Foreground
self.lon1 = np.array([])
self.time = np.array([])
self.col = np.array([])
- return
def clearbg(self) -> None: # Background
"""Clear the background trail segment buffers."""
@@ -243,7 +239,6 @@ def clearbg(self) -> None: # Background
self.bglon1 = np.array([])
self.bgtime = np.array([])
self.bgacid = []
- return
def clear(self) -> None:
"""Clear all trail data: foreground, background and new-line buffers."""
@@ -252,7 +247,6 @@ def clear(self) -> None:
self.clearfg()
self.clearbg()
self.clearnew()
- return
def setTrails(self, *args: Any) -> bool | tuple[bool, str]:
"""Switch trails on/off, or change the trail color of an aircraft.
@@ -307,7 +301,6 @@ def changeTrailColor(self, color: str, idx: int) -> None:
idx: Aircraft index.
"""
self.accolor[idx] = self.colorList[color]
- return
def reset(self) -> None:
"""Clear all trail data and switch trails off upon simulation reset."""
diff --git a/packages/minisky/minisky/traffic/wind.py b/packages/minisky/minisky/traffic/wind.py
index 0677bc1..11c4e46 100644
--- a/packages/minisky/minisky/traffic/wind.py
+++ b/packages/minisky/minisky/traffic/wind.py
@@ -71,7 +71,6 @@ def __init__(self) -> None:
# Clear actual field
self.clear()
- return
def clear(self) -> None: # Clear actual field
"""Remove all wind vectors, leaving a windless (winddim 0) field."""
@@ -86,7 +85,6 @@ def clear(self) -> None: # Clear actual field
self.nvec = 0
self.fe = None
self.fn = None
- return
def addpointvne(
self,
@@ -156,7 +154,7 @@ def addpointvne(
bounds_error=False,
fill_value=0.0,
)
- except Exception:
+ except Exception: # ruff: ignore[BLE001] scipy interpolation may fail broadly
# Create vn, ve if RGI is not possible
vnaxis = fnorth(self.altaxis).T
veaxis = feast(self.altaxis).T
@@ -404,8 +402,6 @@ def remove(self, idx: int) -> None: # remove a point using the returned index w
if self.winddim < 3 or len(self.iprof) == 0 or len(self.lat) == 0:
self.winddim = min(2, len(self.lat)) # Check for 0, 1D, 2D or 3D
- return
-
class Wind(TrafficArrays, Windfield):
"""Wind field with the stack-command interface of the simulation.
@@ -484,6 +480,6 @@ def get(self, lat: Lat, lon: Lon, alt: Alt | None = None) -> tuple[bool, str]:
wdir = (np.degrees(np.arctan2(ve, vn)) + 180.0) % 360.0
wspd = np.sqrt(vn * vn + ve * ve)
- txt = f"WIND AT {lat:.5f}, {lon:.5f}: {int(round(wdir)):03d}/{int(round(wspd / kts))}"
+ txt = f"WIND AT {lat:.5f}, {lon:.5f}: {round(wdir):03d}/{round(wspd / kts)}"
return True, txt
diff --git a/pyproject.toml b/pyproject.toml
index 98c35d6..9aa8876 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ members = ["packages/*"]
dev = [
"ipykernel>=6.29.5",
"pytest>=9.1.1",
- "ruff>=0.8.0",
+ "ruff>=0.16.0",
"pyright>=1.1.390",
"fakeredis>=2.26",
]
@@ -27,31 +27,7 @@ line-length = 100
target-version = "py311"
[tool.ruff.lint]
-# E/W: pycodestyle, F: pyflakes, I: isort, UP: pyupgrade,
-# B: flake8-bugbear, SIM: flake8-simplify, C4: flake8-comprehensions,
-# DTZ: flake8-datetimez (no naive datetimes; sim.utc is timezone-aware UTC)
-select = ["E", "W", "F", "I", "UP", "B", "SIM", "C4", "DTZ"]
-ignore = [
- "E501", # line length is handled by the formatter
- "B008", # FastAPI relies on function calls (Depends/File) in argument defaults
- "E711", # `== None` on numpy arrays is elementwise, not equivalent to `is None`
- "E712", # `== True/False` on numpy arrays is elementwise and intentional
- "E721", # `type(x) == y` comparisons are intentional in a few numeric paths
-]
-
-[tool.ruff.lint.per-file-ignores]
-"__init__.py" = ["F401"] # re-exports
-"packages/minisky/minisky/plugin/*.py" = ["F401"] # plugin API surface re-exports
-
-[tool.ruff.lint.isort]
-known-first-party = [
- "minisky",
- "minisky_example",
- "minisky_example_customautopilot",
- "minisky_tangram",
- "example_plugins",
- "tests",
-]
+extend-select = ["NPY"]
[tool.pyright]
include = [
diff --git a/tests/conftest.py b/tests/conftest.py
index b8d8754..c15212c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,10 +10,10 @@
from collections.abc import Callable, Iterator
import pytest
-
from minisky import MiniSky
from minisky.core.config import MiniSkyConfig
from minisky.simulation import Simulation
+
from tests._types import RunCommand, StepUntil
diff --git a/tests/integration/test_conflict.py b/tests/integration/test_conflict.py
index 66bb171..26f319d 100644
--- a/tests/integration/test_conflict.py
+++ b/tests/integration/test_conflict.py
@@ -3,10 +3,10 @@
from __future__ import annotations
import pytest
-
from minisky import MiniSky
from minisky.simulation import Simulation
from minisky.traffic.asas import MVP
+
from tests._types import RunCommand, StepUntil
FT = 0.3048
diff --git a/tests/integration/test_navdata.py b/tests/integration/test_navdata.py
index 76fb727..9abf648 100644
--- a/tests/integration/test_navdata.py
+++ b/tests/integration/test_navdata.py
@@ -62,7 +62,7 @@ def test_defwpt_delete_via_lon_delete_keyword(self, runtime: MiniSky, sim: Simul
def test_defwpt_delete_via_wptype_del(self, runtime: MiniSky, sim: Simulation) -> None:
navdb = runtime.navigation
navdb.defwpt("TSTWPT3", 52.0, 4.0)
- ok, msg = navdb.defwpt("TSTWPT3", 52.0, 4.0, "DEL")
+ ok, _msg = navdb.defwpt("TSTWPT3", 52.0, 4.0, "DEL")
assert ok
assert "TSTWPT3" not in navdb.wpid
diff --git a/tests/integration/test_plugin.py b/tests/integration/test_plugin.py
index 6150261..0db5566 100644
--- a/tests/integration/test_plugin.py
+++ b/tests/integration/test_plugin.py
@@ -12,13 +12,12 @@
import numpy as np
import pytest
-from pydantic import BaseModel
-
from minisky import MiniSky, MiniSkyConfig
from minisky import plugin as plugin_api
from minisky.simulation import Simulation
from minisky.traffic import Traffic
from minisky.traffic.autopilot import Autopilot
+from pydantic import BaseModel
@pytest.fixture
diff --git a/tests/integration/test_route_autopilot.py b/tests/integration/test_route_autopilot.py
index ca5334c..d159cc6 100644
--- a/tests/integration/test_route_autopilot.py
+++ b/tests/integration/test_route_autopilot.py
@@ -2,13 +2,15 @@
from __future__ import annotations
+from typing import ClassVar
+
import numpy as np
import pytest
-
from minisky import MiniSky
from minisky.tools import geo
from minisky.traffic import route as route_commands
from minisky.traffic.route import Route
+
from tests._types import RunCommand, StepUntil
FT = 0.3048
@@ -298,7 +300,12 @@ class TestWaypointSwitching:
"""
# Zig-zag legs of ~2 nm force a real heading change at every waypoint
- WPTS = [(52.00, 4.05), (52.03, 4.10), (52.00, 4.15), (52.03, 4.20)]
+ WPTS: ClassVar[list[tuple[float, float]]] = [
+ (52.00, 4.05),
+ (52.03, 4.10),
+ (52.00, 4.15),
+ (52.03, 4.20),
+ ]
@pytest.fixture
def route(self, runtime: MiniSky, run_cmd: RunCommand, aircraft: str) -> Route:
diff --git a/tests/integration/test_scenario.py b/tests/integration/test_scenario.py
index 13b5e27..a76201d 100644
--- a/tests/integration/test_scenario.py
+++ b/tests/integration/test_scenario.py
@@ -3,8 +3,8 @@
from __future__ import annotations
import pytest
-
from minisky import MiniSky
+
from tests._types import RunCommand, StepUntil
FT = 0.3048
diff --git a/tests/integration/test_stack.py b/tests/integration/test_stack.py
index acecdbe..4c95658 100644
--- a/tests/integration/test_stack.py
+++ b/tests/integration/test_stack.py
@@ -7,9 +7,9 @@
from pathlib import Path
import pytest
-
from minisky import MiniSky
from minisky.simulation import Simulation
+
from tests._types import RunCommand
FT = 0.3048
diff --git a/tests/integration/test_streaming.py b/tests/integration/test_streaming.py
index 5ed787a..705d69b 100644
--- a/tests/integration/test_streaming.py
+++ b/tests/integration/test_streaming.py
@@ -9,10 +9,10 @@
import json
import pytest
-
from minisky import MiniSky
from minisky.simulation import Simulation, SimulationState
from minisky.streaming import STREAM_MAX_HZ, StreamHub, build_snapshot
+
from tests._types import RunCommand
diff --git a/tests/integration/test_tangram_bridge.py b/tests/integration/test_tangram_bridge.py
index fc15afe..4370e31 100644
--- a/tests/integration/test_tangram_bridge.py
+++ b/tests/integration/test_tangram_bridge.py
@@ -7,11 +7,10 @@
import fakeredis
import pytest
-from redis.client import PubSub
-
from minisky import MiniSky
from minisky.simulation import Simulation, SimulationState
from minisky_tangram import TangramBridge
+from redis.client import PubSub
Observer = tuple[fakeredis.FakeRedis, PubSub]
StepUntil = Callable[[Callable[[], bool]], int]
diff --git a/tests/integration/test_traffic.py b/tests/integration/test_traffic.py
index 6ab058f..68a464e 100644
--- a/tests/integration/test_traffic.py
+++ b/tests/integration/test_traffic.py
@@ -4,9 +4,9 @@
import numpy as np
import pytest
-
from minisky import MiniSky
from minisky.simulation import Simulation
+
from tests._types import RunCommand
FT = 0.3048
@@ -15,7 +15,9 @@
class TestCreate:
def test_cre_single(self, runtime: MiniSky, sim: Simulation) -> None:
- ok, msg = runtime.traffic.cre("KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150)
+ ok, _msg = runtime.traffic.cre(
+ "KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150
+ )
assert ok
assert runtime.traffic.ntraf == 1
assert runtime.traffic.callsign[0] == "KL001"
@@ -29,7 +31,7 @@ def test_cre_lowercase_callsign_is_uppercased(self, runtime: MiniSky, sim: Simul
def test_cre_duplicate_callsign_rejected(self, runtime: MiniSky, sim: Simulation) -> None:
runtime.traffic.cre("KL001")
- ok, msg = runtime.traffic.cre("KL001")
+ ok, _msg = runtime.traffic.cre("KL001")
assert not ok
assert runtime.traffic.ntraf == 1
diff --git a/tests/test_api.py b/tests/test_api.py
index be08a1e..6539c2b 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -13,7 +13,6 @@
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
-
from minisky import MiniSky, MiniSkyConfig
pytestmark = pytest.mark.api
diff --git a/tests/unit/test_aero.py b/tests/unit/test_aero.py
index c75e9e2..a9f0485 100644
--- a/tests/unit/test_aero.py
+++ b/tests/unit/test_aero.py
@@ -5,7 +5,6 @@
import numpy as np
import pytest
-
from minisky.tools import aero
@@ -89,12 +88,12 @@ def test_mach_increases_with_altitude_at_constant_tas(self):
assert aero.vtas2mach(200.0, 10000.0) > aero.vtas2mach(200.0, 0.0)
def test_vcasormach_interprets_small_value_as_mach(self):
- tas, cas, mach = aero.vcasormach(0.8, 10000.0, aero.DEFAULT_CASMACH_THRESHOLD)
+ tas, _cas, mach = aero.vcasormach(0.8, 10000.0, aero.DEFAULT_CASMACH_THRESHOLD)
assert mach == pytest.approx(0.8, rel=1e-6)
assert tas > 200.0
def test_vcasormach_interprets_large_value_as_cas(self):
- tas, cas, mach = aero.vcasormach(150.0, 5000.0, aero.DEFAULT_CASMACH_THRESHOLD)
+ tas, cas, _mach = aero.vcasormach(150.0, 5000.0, aero.DEFAULT_CASMACH_THRESHOLD)
assert cas == pytest.approx(150.0, rel=1e-6)
assert tas > cas
diff --git a/tests/unit/test_areafilter.py b/tests/unit/test_areafilter.py
index f359f83..d6ba8d3 100644
--- a/tests/unit/test_areafilter.py
+++ b/tests/unit/test_areafilter.py
@@ -6,7 +6,6 @@
import numpy as np
import pytest
-
from minisky.tools.areafilter import AreaFilter
@@ -23,7 +22,7 @@ def check_single(
class TestDefineArea:
def test_define_box_and_has_area(self, area_filter: AreaFilter) -> None:
- ok, msg = area_filter.define_area("BOX1", "BOX", [52.0, 4.0, 53.0, 5.0])
+ ok, _msg = area_filter.define_area("BOX1", "BOX", [52.0, 4.0, 53.0, 5.0])
assert ok
assert area_filter.has_area("BOX1")
diff --git a/tests/unit/test_convert.py b/tests/unit/test_convert.py
index e1d9b10..91d9d6c 100644
--- a/tests/unit/test_convert.py
+++ b/tests/unit/test_convert.py
@@ -5,7 +5,6 @@
"""
import pytest
-
from minisky.tools import convert as cv
FT = 0.3048
diff --git a/tests/unit/test_detection.py b/tests/unit/test_detection.py
index e1ef486..b35ef81 100644
--- a/tests/unit/test_detection.py
+++ b/tests/unit/test_detection.py
@@ -17,7 +17,6 @@
import numpy as np
import pytest
-
from minisky.tools import geo
from minisky.tools.aero import ft, nm
from minisky.traffic.asas.detection import ConflictDetection
diff --git a/tests/unit/test_geo.py b/tests/unit/test_geo.py
index 7a64617..dd18851 100644
--- a/tests/unit/test_geo.py
+++ b/tests/unit/test_geo.py
@@ -6,7 +6,6 @@
import numpy as np
import pytest
-
from minisky.tools import geo
NM_IN_M = 1852.0
diff --git a/tests/unit/test_phase.py b/tests/unit/test_phase.py
index 8a2e12a..2e86c94 100644
--- a/tests/unit/test_phase.py
+++ b/tests/unit/test_phase.py
@@ -5,7 +5,6 @@
"""
import numpy as np
-
from minisky.traffic.performance import phase
from minisky.traffic.performance.coeff import LIFT_FIXWING, LIFT_ROTOR
diff --git a/tests/unit/test_tangram_plugin.py b/tests/unit/test_tangram_plugin.py
index e28aa63..bdf62f2 100644
--- a/tests/unit/test_tangram_plugin.py
+++ b/tests/unit/test_tangram_plugin.py
@@ -1,7 +1,6 @@
"""Unit tests for the pure functions of the tangram bridge plugin."""
import pytest
-
from minisky.streaming import Snapshot
from minisky_tangram import convert_snapshot, extract_command
diff --git a/uv.lock b/uv.lock
index 04d50ce..031c6e0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -31,7 +31,7 @@ dev = [
{ name = "ipykernel", specifier = ">=6.29.5" },
{ name = "pyright", specifier = ">=1.1.390" },
{ name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.8.0" },
+ { name = "ruff", specifier = ">=0.16.0" },
]
docs = [
{ name = "mkdocstrings-python", specifier = ">=2.0.5" },
@@ -2517,27 +2517,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.20"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
- { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
- { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
- { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
- { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
- { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
- { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
- { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
- { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
- { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
- { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
- { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
- { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
- { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
- { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
- { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
- { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
+version = "0.16.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" },
+ { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" },
+ { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" },
+ { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" },
]
[[package]]
From e0be38822e0ebf430896719b3b6bb23ef2e00c84 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 00:32:48 +0800
Subject: [PATCH 3/8] ci: setup ruff and pyright
---
.github/workflows/tests.yml | 38 ++++++++++++++++++++++++++++---------
1 file changed, 29 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 1bb70ec..e9ea7b8 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,15 +1,23 @@
-name: tests
+name: ci
on:
- pull_request:
push:
- branches: [main, refactor]
+ branches:
+ - main
+ pull_request:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
- pytest:
+ test-python:
+ name: test python
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - name: Checkout repository
+ uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v6
@@ -20,19 +28,31 @@ jobs:
node-version: 24
cache: pnpm
- - name: Build frontend plugin
+ - name: Install frontend dependencies
+ run: pnpm install --frozen-lockfile
+
+ # NOTE(abraham): we must build the frontend assets otherwise tangram_minisky would fail
+ # with missing frontend assets.
+ # we should document this and potentially split up the action into repo-with-frontend
+ # just like tangram
+ - name: Check and build frontend plugin
run: |
- pnpm install --frozen-lockfile
pnpm check
pnpm build
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install Python dependencies
- run: uv sync --locked --all-packages
+ run: uv sync --all-groups --all-packages
+
+ - name: Check code quality
+ run: |
+ uv run ruff check packages tests
+ uv run ruff format packages tests --check
+ uv run pyright
- name: Run unit and integration tests
run: uv run pytest
From c8ae28a176c276c81d4a505889fc1d52c5cd8215 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 11:24:53 +0800
Subject: [PATCH 4/8] refactor!: replace Go-like `ok, err` with Rust-like
Result enum
we promise py310 anyway so we can use `match` nicely
note that the REST API still uses *internally tagged enums* for compatibility
---
docs/guides/plugins.md | 30 +++-
.../src/minisky_example/__init__.py | 9 +-
.../src/minisky_tangram/__init__.py | 21 ++-
packages/minisky/minisky/__init__.py | 5 +
.../minisky/minisky/core/trafficarrays.py | 13 +-
packages/minisky/minisky/core/varexplorer.py | 9 +-
packages/minisky/minisky/plugin/plugin.py | 35 ++--
packages/minisky/minisky/result.py | 88 ++++++++++
packages/minisky/minisky/server.py | 41 ++++-
packages/minisky/minisky/simulation/runner.py | 12 +-
.../minisky/minisky/simulation/simulation.py | 26 ++-
packages/minisky/minisky/stack/__init__.py | 76 ++++-----
packages/minisky/minisky/stack/argparser.py | 7 +-
packages/minisky/minisky/tools/areafilter.py | 40 ++---
packages/minisky/minisky/tools/geo.py | 11 +-
packages/minisky/minisky/tools/navdata.py | 27 ++-
packages/minisky/minisky/tools/position.py | 11 +-
.../minisky/minisky/traffic/asas/detection.py | 63 +++----
packages/minisky/minisky/traffic/asas/mvp.py | 49 ++----
.../minisky/traffic/asas/resolution.py | 138 ++++++---------
packages/minisky/minisky/traffic/autopilot.py | 125 +++++---------
.../minisky/traffic/performance/perfoap.py | 25 ++-
packages/minisky/minisky/traffic/route.py | 160 ++++++++----------
packages/minisky/minisky/traffic/traffic.py | 134 +++++----------
.../minisky/minisky/traffic/trafficgroups.py | 45 +++--
packages/minisky/minisky/traffic/trails.py | 15 +-
packages/minisky/minisky/traffic/wind.py | 16 +-
tests/integration/test_conflict.py | 21 +--
tests/integration/test_navdata.py | 24 +--
tests/integration/test_plugin.py | 94 +++++-----
tests/integration/test_route_autopilot.py | 4 +-
tests/integration/test_stack.py | 6 +-
tests/integration/test_streaming.py | 6 +-
tests/integration/test_tangram_bridge.py | 4 +-
tests/integration/test_traffic.py | 23 +--
tests/unit/test_areafilter.py | 4 +-
36 files changed, 677 insertions(+), 740 deletions(-)
create mode 100644 packages/minisky/minisky/result.py
diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md
index bd51d5b..e65745f 100644
--- a/docs/guides/plugins.md
+++ b/docs/guides/plugins.md
@@ -63,14 +63,18 @@ Use [`@plugin.command`][minisky.plugin.plugin_decorators.command] on an instance
Use the `arguments` option when MiniSky's stack parser needs more information than the Python annotations provide:
```python
+from minisky import Err, Ok, Result
+
@plugin_api.command(arguments="txt,[int]")
-def passengers(self, callsign: str, count: int = -1) -> tuple[bool, str]:
+def passengers(self, callsign: str, count: int = -1) -> Result[str, str]:
"""Set or get the passenger count for an aircraft."""
- ...
+ if count < 0:
+ return Ok("current passenger count")
+ if count > 500:
+ return Err("passenger count is too large")
+ return Ok("passenger count updated")
```
-A command handler can return `(success, message)`. Returning `None` means the command completed successfully without a message.
-
## Add simulation hooks
Use [`@plugin.hook`][minisky.plugin.plugin_decorators.hook] for work tied to the simulation cycle:
@@ -138,17 +142,25 @@ async with MiniSky(config=config) as runtime:
To load only one installed plugin, call `load()` with its plugin ID:
```python
-ok, message = await runtime.plugins.load("example")
-print(message)
+from minisky import Err, Ok
+
+match await runtime.plugins.load("example"):
+ case Ok(message):
+ print(message)
+ case Err(error):
+ # ... handle the error
```
-Plugin IDs are case-insensitive. `load()` returns a success flag and a message, so you can decide how your application should handle a missing, invalid, or already loaded plugin.
+Plugin IDs are case-insensitive. `load()` returns [`Result[str, str]`][minisky.result.Result]`.
To inspect the plugins known to the runtime, use `listing()`:
```python
-ok, text = runtime.plugins.listing()
-print(text)
+match runtime.plugins.listing():
+ case Ok(text):
+ print(text)
+ case Err(error):
+ # ... handle the error
```
While the simulator is running, you can manage plugins through the stack instead:
diff --git a/packages/minisky-example/src/minisky_example/__init__.py b/packages/minisky-example/src/minisky_example/__init__.py
index 3070418..780521b 100644
--- a/packages/minisky-example/src/minisky_example/__init__.py
+++ b/packages/minisky-example/src/minisky_example/__init__.py
@@ -6,6 +6,7 @@
import numpy as np
from minisky import plugin as plugin_api
+from minisky.result import Err, Ok, Result
# --8<-- [start:declaration]
@@ -39,16 +40,16 @@ def update(self) -> None:
self.updates += 1
@plugin_api.command(arguments="txt,[int]")
- def passengers(self, callsign: str, count: int = -1) -> tuple[bool, str]:
+ def passengers(self, callsign: str, count: int = -1) -> Result[str, str]:
"""Set or get the number of passengers on an aircraft."""
callsign = callsign.upper()
if callsign not in self.traffic.callsign:
- return False, f"Aircraft {callsign} not found"
+ return Err(f"Aircraft {callsign} not found")
index = self.traffic.callsign.index(callsign)
if count < 0:
- return True, f"Aircraft {callsign} has {int(self.npassengers[index])} passengers"
+ return Ok(f"Aircraft {callsign} has {int(self.npassengers[index])} passengers")
self.npassengers[index] = count
- return True, f"Set {callsign} passengers to {count}"
+ return Ok(f"Set {callsign} passengers to {count}")
# --8<-- [end:entity]
diff --git a/packages/minisky-tangram/src/minisky_tangram/__init__.py b/packages/minisky-tangram/src/minisky_tangram/__init__.py
index 31f9d68..6e3f069 100644
--- a/packages/minisky-tangram/src/minisky_tangram/__init__.py
+++ b/packages/minisky-tangram/src/minisky_tangram/__init__.py
@@ -50,6 +50,7 @@
from typing import Any, TypedDict, cast
from minisky import plugin as plugin_api
+from minisky.result import Err, Ok, Result
from minisky.simulation import SimulationState
from minisky.streaming import Snapshot
from minisky.tools.aero import fpm, ft, kts
@@ -236,7 +237,7 @@ def __init__(
self._thread: threading.Thread | None = None
self.ready = threading.Event()
- def start(self, runtime: plugin_api.PluginRuntime) -> tuple[bool, str]:
+ def start(self, runtime: plugin_api.PluginRuntime) -> Result[str, str]:
"""Bind runtime capabilities and start the Redis thread."""
try:
if self.redis_factory is None:
@@ -247,7 +248,7 @@ def start(self, runtime: plugin_api.PluginRuntime) -> tuple[bool, str]:
redis.Redis.from_url, # pyright: ignore[reportUnknownMemberType]
)
except ImportError:
- return False, (
+ return Err(
"TANGRAM plugin needs the redis package; run `just sync` from the "
"MiniSky repository root"
)
@@ -259,7 +260,7 @@ def start(self, runtime: plugin_api.PluginRuntime) -> tuple[bool, str]:
self.ready.clear()
self._thread = threading.Thread(target=self._run, name="tangram-bridge", daemon=True)
self._thread.start()
- return True, f"Tangram bridge publishing to to:{self.channel}:* at {self.redis_url}"
+ return Ok(f"Tangram bridge publishing to to:{self.channel}:* at {self.redis_url}")
def stop(self) -> None:
"""Stop Redis I/O and release runtime callbacks."""
@@ -276,7 +277,7 @@ def stop(self) -> None:
self._stack_command = None
@plugin_api.command(name="TANGRAM")
- def status(self) -> tuple[bool, str]:
+ def status(self) -> Result[str, str]:
"""Show the status of the tangram Redis bridge."""
status = "connected" if self.connected else "disconnected"
text = (
@@ -285,7 +286,7 @@ def status(self) -> tuple[bool, str]:
)
if self.last_error:
text += f"\nLast error: {self.last_error}"
- return True, text
+ return Ok(text)
@plugin_api.hook("update")
def tick(self) -> None:
@@ -417,10 +418,12 @@ def build(context: plugin_api.PluginContext[TangramConfig]) -> plugin_api.Plugin
@asynccontextmanager
async def lifespan(runtime: plugin_api.PluginRuntime) -> AsyncGenerator[None]:
runtime.subscribe_console(bridge.capture_console)
- success, message = bridge.start(runtime)
- runtime.echo(message)
- if not success:
- raise RuntimeError(message)
+ match bridge.start(runtime):
+ case Ok(message):
+ runtime.echo(message)
+ case Err(message):
+ runtime.echo(message)
+ raise RuntimeError(message)
try:
yield
finally:
diff --git a/packages/minisky/minisky/__init__.py b/packages/minisky/minisky/__init__.py
index 8289e7b..3e9d5e3 100644
--- a/packages/minisky/minisky/__init__.py
+++ b/packages/minisky/minisky/__init__.py
@@ -10,6 +10,7 @@
default_user_config_dir,
default_user_config_toml_path,
)
+from minisky.result import Err, Ok, Result, UnwrapError
from minisky.runtime import MiniSky
from minisky.simulation import SimulationState
@@ -23,9 +24,13 @@
"BS_CMDERR",
"BS_FUNERR",
"BS_OK",
+ "Err",
"MiniSky",
"MiniSkyConfig",
+ "Ok",
+ "Result",
"SimulationState",
+ "UnwrapError",
"default_user_config_dir",
"default_user_config_toml_path",
)
diff --git a/packages/minisky/minisky/core/trafficarrays.py b/packages/minisky/minisky/core/trafficarrays.py
index 04d8220..6efcd92 100644
--- a/packages/minisky/minisky/core/trafficarrays.py
+++ b/packages/minisky/minisky/core/trafficarrays.py
@@ -32,6 +32,7 @@
import numpy as np
from minisky.identifiers import normalize_public_name
+from minisky.result import Err, Ok, Result
defaults = MappingProxyType({"float": 0.0, "int": 0, "uint": 0, "bool": False, "S": "", "str": ""})
@@ -178,18 +179,18 @@ def remove(self, replacements: tuple[PreparedReplacement, ...]) -> None:
if implementations.get(replacement.name) is replacement.implementation:
del implementations[replacement.name]
- def select(self, basename: str = "", implname: str = "") -> tuple[bool, str]:
+ def select(self, basename: str = "", implname: str = "") -> Result[str, str]:
if not basename:
- return True, "Replaceable classes in MiniSky:\n" + ", ".join(sorted(self._bases))
+ return Ok("Replaceable classes in MiniSky:\n" + ", ".join(sorted(self._bases)))
base = self._bases.get(basename.upper())
if base is None:
- return False, f"Replaceable {basename} not found."
+ return Err(f"Replaceable {basename} not found.")
implementations = self._implementations[base]
slot = self._slots[base]
current = type(slot.current)
if not implname:
- return True, (
+ return Ok(
f"Current implementation for {basename}: {current.__name__}\n"
f"Available implementations: {', '.join(sorted(implementations))}"
)
@@ -197,10 +198,10 @@ def select(self, basename: str = "", implname: str = "") -> tuple[bool, str]:
requested = base.__name__.upper() if implname.upper() == "BASE" else implname.upper()
implementation = implementations.get(requested)
if implementation is None:
- return False, f"Implementation {implname} not found for {basename}."
+ return Err(f"Implementation {implname} not found for {basename}.")
if current is not implementation:
slot.replace(implementation)
- return True, f"Selected {implname} for {basename}"
+ return Ok(f"Selected {implname} for {basename}")
def reset(self) -> None:
for base, slot in self._slots.items():
diff --git a/packages/minisky/minisky/core/varexplorer.py b/packages/minisky/minisky/core/varexplorer.py
index ef24e33..137c6a3 100644
--- a/packages/minisky/minisky/core/varexplorer.py
+++ b/packages/minisky/minisky/core/varexplorer.py
@@ -20,6 +20,7 @@
import numpy as np
from minisky.core import TrafficArrays
+from minisky.result import Err, Ok, Result
class VariableExplorer:
@@ -62,13 +63,13 @@ def unregister_data_parent(self, name: str, *, expected: object | None = None) -
if current is not None and (expected is None or current[0] is expected):
del self.varlist[name]
- def lsvar(self, varname: str = "") -> tuple[bool, str]:
+ def lsvar(self, varname: str = "") -> Result[str, str]:
"""Stack function to list information on simulation variables in the
BlueSky console."""
if not varname:
# When no argument is passed, show a list of parent objects for which
# variables can be accessed
- return True, "\n" + str.join(", ", list(self.varlist))
+ return Ok("\n" + str.join(", ", list(self.varlist)))
# Find the variable in the variable list
v = self.findvar(varname)
@@ -85,8 +86,8 @@ def lsvar(self, varname: str = "") -> tuple[bool, str]:
txt += f"Parent: {v.parentname}"
if attrs:
txt += "\nAttributes: " + str.join(", ", attrs) + "\n"
- return True, "\n" + txt
- return False, f"Variable {varname} not found"
+ return Ok("\n" + txt)
+ return Err(f"Variable {varname} not found")
def findvar(self, varname: str) -> Variable | None:
"""Find a variable and its parent object in the registered varlist set, based
diff --git a/packages/minisky/minisky/plugin/plugin.py b/packages/minisky/minisky/plugin/plugin.py
index e736dd2..9a97299 100644
--- a/packages/minisky/minisky/plugin/plugin.py
+++ b/packages/minisky/minisky/plugin/plugin.py
@@ -28,6 +28,7 @@
declared_hooks,
declared_replacement,
)
+from minisky.result import Err, Ok, Result
from minisky.streaming import Snapshot, build_snapshot
if TYPE_CHECKING:
@@ -40,7 +41,7 @@
ConfigT = TypeVar("ConfigT")
ComponentT = TypeVar("ComponentT")
-CommandReply = tuple[bool, str]
+CommandReply = Result[str, str]
class PluginError(RuntimeError):
@@ -328,19 +329,19 @@ def discover(self) -> None:
if existing is None or not existing.loaded:
self.plugins.pop(plugin_name, None)
- async def load(self, name: str) -> CommandReply:
+ async def load(self, name: str) -> Result[str, str]:
"""Load a discovered plugin by name."""
async with self._lock:
if self._state is not _ManagerState.OPEN:
- return False, "Plugin manager is closed"
+ return Err("Plugin manager is closed")
plugin = self.plugins.get(name.upper())
if plugin is None:
- return False, f"Error loading plugin: plugin {name} not found."
+ return Err(f"Error loading plugin: plugin {name} not found.")
if plugin.loaded:
- return False, f"Plugin {plugin.plugin_name} already loaded"
+ return Err(f"Plugin {plugin.plugin_name} already loaded")
return await self._load(plugin)
- async def _load(self, plugin: _PluginRecord) -> CommandReply:
+ async def _load(self, plugin: _PluginRecord) -> Result[str, str]:
prepared: _PreparedPlugin | None = None
plugin_runtime: PluginRuntime | None = None
lifespan: AbstractAsyncContextManager[None] | None = None
@@ -375,7 +376,7 @@ async def _load(self, plugin: _PluginRecord) -> CommandReply:
plugin.lifespan = lifespan
plugin.runtime = plugin_runtime
self.loaded_plugins[plugin.plugin_name] = plugin
- return True, f"Successfully loaded plugin {plugin.plugin_name}"
+ return Ok(f"Successfully loaded plugin {plugin.plugin_name}")
except BaseException as exc:
if prepared is not None:
prepared.abort()
@@ -389,7 +390,7 @@ async def _load(self, plugin: _PluginRecord) -> CommandReply:
if not isinstance(exc, Exception):
raise
traceback.print_exception(exc)
- return False, f"Error loading {plugin.plugin_name}: {exc}"
+ return Err(f"Error loading {plugin.plugin_name}: {exc}")
def _build(self, key: str, declaration: Plugin) -> PluginSpec:
raw = deepcopy(self.config.plugins.get(key, {}))
@@ -542,13 +543,15 @@ async def load_configured(self) -> tuple[str, ...]:
"""Attempt every configured plugin and return those loaded successfully."""
loaded: list[str] = []
for plugin_name in self.config.plugins:
- ok, message = await self.load(plugin_name)
- self.console.echo(message)
- if ok:
- loaded.append(plugin_name.upper())
+ match await self.load(plugin_name):
+ case Ok(message):
+ self.console.echo(message)
+ loaded.append(plugin_name.upper())
+ case Err(message):
+ self.console.echo(message)
return tuple(loaded)
- def listing(self) -> CommandReply:
+ def listing(self) -> Result[str, str]:
running = set(self.loaded_plugins)
available = set(self.plugins) - running
text = f"\nLoaded plugins: {', '.join(sorted(running)) if running else '(none)'}"
@@ -556,7 +559,7 @@ def listing(self) -> CommandReply:
text += f"\nAvailable plugins: {', '.join(sorted(available))}"
else:
text += "\nNo additional plugins available."
- return True, text
+ return Ok(text)
def manage(
self, command: str = "LIST", plugin_name: str = ""
@@ -567,11 +570,11 @@ def manage(
return self.listing()
if operation == "LOAD":
if not plugin_name.strip():
- return False, "plugin name is required"
+ return Err("plugin name is required")
return self.load(plugin_name)
if not plugin_name:
return self.load(command)
- return False, f"Unknown command: {command}"
+ return Err(f"Unknown command: {command}")
def preupdate(self) -> None:
self._run_hooks("preupdate")
diff --git a/packages/minisky/minisky/result.py b/packages/minisky/minisky/result.py
new file mode 100644
index 0000000..879795a
--- /dev/null
+++ b/packages/minisky/minisky/result.py
@@ -0,0 +1,88 @@
+"""Typed success/error result values.
+
+See:
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Generic, Literal, Never, TypeAlias, TypeVar
+
+ValueT_co = TypeVar("ValueT_co", covariant=True)
+ErrorT_co = TypeVar("ErrorT_co", covariant=True)
+
+
+@dataclass(frozen=True, slots=True)
+class Ok(Generic[ValueT_co]):
+ """A successful result containing a value."""
+
+ _value: ValueT_co
+
+ def ok(self) -> ValueT_co:
+ """Return the success value."""
+ return self._value
+
+ def err(self) -> None:
+ """Return no error value for a successful result."""
+ return
+
+ def is_ok(self) -> Literal[True]:
+ """Return whether this result is successful."""
+ return True
+
+ def is_err(self) -> Literal[False]:
+ """Return whether this result is an error."""
+ return False
+
+ def unwrap(self) -> ValueT_co:
+ """Return the success value."""
+ return self._value
+
+ def unwrap_err(self) -> Never:
+ """Raise because this result does not contain an error."""
+ raise UnwrapError(
+ self, f"called `Result.unwrap_err()` on successful result: {self._value!r}"
+ )
+
+
+@dataclass(frozen=True, slots=True)
+class Err(Generic[ErrorT_co]):
+ """An unsuccessful result containing an error value."""
+
+ _value: ErrorT_co
+
+ def ok(self) -> None:
+ """Return no success value for an unsuccessful result."""
+ return
+
+ def err(self) -> ErrorT_co:
+ """Return the error value."""
+ return self._value
+
+ def is_ok(self) -> Literal[False]:
+ """Return whether this result is successful."""
+ return False
+
+ def is_err(self) -> Literal[True]:
+ """Return whether this result is an error."""
+ return True
+
+ def unwrap(self) -> Never:
+ """Raise because this result does not contain a success value."""
+ raise UnwrapError(self, f"called `Result.unwrap()` on errored result: {self._value!r}")
+
+ def unwrap_err(self) -> ErrorT_co:
+ """Return the error value."""
+ return self._value
+
+
+class UnwrapError(Exception):
+ """Raised when unwrapping the absent side of a result."""
+
+ def __init__(self, result: Any, message: str) -> None:
+ self.result = result
+ super().__init__(message)
+
+
+Result: TypeAlias = Ok[ValueT_co] | Err[ErrorT_co]
+"""A value that is either successful (`Ok`) or unsuccessful (`Err`)."""
diff --git a/packages/minisky/minisky/server.py b/packages/minisky/minisky/server.py
index a71a9bc..6b02dab 100644
--- a/packages/minisky/minisky/server.py
+++ b/packages/minisky/minisky/server.py
@@ -25,7 +25,7 @@
import os
from contextlib import asynccontextmanager, suppress
from io import StringIO
-from typing import Annotated, Any, cast
+from typing import Annotated, Any, Literal, TypeAlias, TypedDict, cast
import pandas as pd
from fastapi import (
@@ -43,6 +43,7 @@
from fastapi.staticfiles import StaticFiles
from minisky import MiniSky
+from minisky.result import Err, Ok, Result
from minisky.tools import aero
@@ -53,6 +54,34 @@ def _get_runtime(request: Request) -> MiniSky:
Runtime = Annotated[MiniSky, Depends(_get_runtime)]
+# we are using adjacently tagged enums for compatability
+# TODO(abraham): use externally tagged once we remove the html
+
+
+class OkResultResponse(TypedDict):
+ """JSON representation of a successful string result."""
+
+ ok: Literal[True]
+ value: str
+
+
+class ErrResultResponse(TypedDict):
+ """JSON representation of an unsuccessful string result."""
+
+ ok: Literal[False]
+ error: str
+
+
+ResultResponse: TypeAlias = OkResultResponse | ErrResultResponse
+
+
+def _result_response(result: Result[str, str]) -> ResultResponse:
+ match result:
+ case Ok(value):
+ return {"ok": True, "value": value}
+ case Err(error):
+ return {"ok": False, "error": error}
+
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -260,14 +289,16 @@ def show_map() -> RedirectResponse:
return RedirectResponse(url="/static/display.html")
-def list_plugins(runtime: Runtime) -> Any:
+def list_plugins(runtime: Runtime) -> ResultResponse:
"""List available and loaded plugins."""
- return runtime.plugins.manage("LIST")
+ result = runtime.plugins.listing()
+ return _result_response(result)
-async def load_plugin(name: str, runtime: Runtime) -> Any:
+async def load_plugin(name: str, runtime: Runtime) -> ResultResponse:
"""Load a plugin by name."""
- return await runtime.plugins.load(name)
+ result = await runtime.plugins.load(name)
+ return _result_response(result)
def create_router() -> APIRouter:
diff --git a/packages/minisky/minisky/simulation/runner.py b/packages/minisky/minisky/simulation/runner.py
index 98b2409..9fa03ef 100644
--- a/packages/minisky/minisky/simulation/runner.py
+++ b/packages/minisky/minisky/simulation/runner.py
@@ -13,6 +13,8 @@
import os
from typing import TYPE_CHECKING
+from minisky.result import Err, Ok, Result
+
if TYPE_CHECKING:
from minisky.simulation.console import ConsoleIO
from minisky.simulation.simulation import Simulation
@@ -73,7 +75,7 @@ def forward(self, seconds: float) -> None:
self.jump_to = self.simulation.simt + seconds - 2 # -2 for the action margin
self.jump = seconds
- def setspeed(self, mult: float) -> tuple[bool, str]:
+ def setspeed(self, mult: float) -> Result[str, str]:
"""Set the simulation speed multiplier (stack DTMULT command).
The loop targets a simulation step every `1 / speed` wall-clock
@@ -84,15 +86,11 @@ def setspeed(self, mult: float) -> tuple[bool, str]:
Args:
mult: Simulation speed factor relative to real time; must be
positive.
-
- Returns:
- Tuple of (success flag, message reporting the new speed, or an
- error message when the multiplier is not positive).
"""
if mult <= 0:
- return False, "DTMULT: speed multiplier must be positive"
+ return Err("DTMULT: speed multiplier must be positive")
self.speed = mult
- return True, f"Simulation speed set to {mult}x"
+ return Ok(f"Simulation speed set to {mult}x")
def prevent_shutdown(self) -> None:
"""Disable shutdown so that `stop` requests are ignored.
diff --git a/packages/minisky/minisky/simulation/simulation.py b/packages/minisky/minisky/simulation/simulation.py
index 846d399..e830a0a 100644
--- a/packages/minisky/minisky/simulation/simulation.py
+++ b/packages/minisky/minisky/simulation/simulation.py
@@ -17,6 +17,8 @@
import numpy as np
+from minisky.result import Err, Ok, Result
+
if TYPE_CHECKING:
from minisky.core.trafficarrays import ReplaceableManager
from minisky.plugin import PluginManager
@@ -218,7 +220,7 @@ def reset(self) -> None:
self.plugins.reset()
self.console.echo("Simulation reset")
- def realtime(self, flag: bool | None = None) -> tuple[bool, str]:
+ def realtime(self, flag: bool | None = None) -> Result[str, str]:
"""Get or set realtime mode (stack REALTIME command).
In realtime mode the timestep may be varied to keep the simulation
@@ -227,15 +229,11 @@ def realtime(self, flag: bool | None = None) -> tuple[bool, str]:
Args:
flag: `True`/`False` to enable or disable realtime mode, or
`None` to only report the current setting.
-
- Returns:
- Tuple of (success flag, message stating whether realtime mode is
- on or off).
"""
if flag is not None:
self.rtmode = flag
- return True, "Realtime mode is o" + ("n" if self.rtmode else "ff")
+ return Ok("Realtime mode is o" + ("n" if self.rtmode else "ff"))
def event(self, eventname: bytes, eventdata: Any, sender_rte: Any) -> bool:
"""Handle events coming from the network.
@@ -274,7 +272,7 @@ def event(self, eventname: bytes, eventdata: Any, sender_rte: Any) -> bool:
return event_processed
- def setutc(self, *args: str) -> tuple[bool, str]:
+ def setutc(self, *args: str) -> Result[str, str]:
"""Set the simulated UTC clock time (stack UTC/DATE command).
Usage: UTC [RUN | REAL | UTC | HH:MM:SS[.ff] | day month year [HH:MM:SS[.ff]]]
@@ -291,10 +289,6 @@ def setutc(self, *args: str) -> tuple[bool, str]:
Args:
*args: Zero, one, three, or four arguments as described above.
-
- Returns:
- Tuple of (success flag, message with the resulting simulation UTC
- time, or an error message when parsing failed).
"""
if not args:
pass # avoid error message, just give time
@@ -317,14 +311,14 @@ def setutc(self, *args: str) -> tuple[bool, str]:
args[0], "%H:%M:%S.%f" if "." in args[0] else "%H:%M:%S"
).replace(tzinfo=datetime.UTC)
except ValueError:
- return False, "Input time invalid"
+ return Err("Input time invalid")
elif len(args) == 3:
day, month, year = args
try:
self.utc = datetime.datetime(int(year), int(month), int(day), tzinfo=datetime.UTC)
except ValueError:
- return False, "Input date invalid."
+ return Err("Input date invalid.")
elif len(args) == 4:
day, month, year, timestring = args
try:
@@ -333,11 +327,11 @@ def setutc(self, *args: str) -> tuple[bool, str]:
("%Y,%m,%d,%H:%M:%S.%f" if "." in timestring else "%Y,%m,%d,%H:%M:%S"),
).replace(tzinfo=datetime.UTC)
except ValueError:
- return False, "Input date invalid."
+ return Err("Input date invalid.")
else:
- return False, "Syntax error"
+ return Err("Syntax error")
- return True, "Simulation UTC " + str(self.utc)
+ return Ok("Simulation UTC " + str(self.utc))
def setseed(self, value: int) -> None:
"""Set the random seed for this simulation (stack SEED command).
diff --git a/packages/minisky/minisky/stack/__init__.py b/packages/minisky/minisky/stack/__init__.py
index 89ed1ad..b30500d 100644
--- a/packages/minisky/minisky/stack/__init__.py
+++ b/packages/minisky/minisky/stack/__init__.py
@@ -34,11 +34,11 @@
from io import StringIO
from pathlib import Path
from threading import Lock
-from typing import TYPE_CHECKING, Any, NamedTuple
+from typing import TYPE_CHECKING, Any
import numpy as np
-from minisky.plugin.plugin_decorators import command as command
+from minisky.result import Err, Ok, Result
from minisky.stack import argparser, commands
from minisky.stack.argparser import ArgumentError, Parameter, String, Time, Txt, getnextarg
@@ -52,11 +52,6 @@
from minisky.traffic import Traffic
-class CommandResult(NamedTuple):
- success: bool
- echotext: str
-
-
class Command:
"""Stack command object.
@@ -97,7 +92,7 @@ def __init__(
self.params = []
self.callback = func
- def __call__(self, argstring: str) -> CommandResult | Awaitable[CommandResult]:
+ def __call__(self, argstring: str) -> Result[str, str] | Awaitable[Result[str, str]]:
"""Parse arguments and execute the callback."""
args: list[Any] = []
param = None
@@ -128,19 +123,22 @@ def __call__(self, argstring: str) -> CommandResult | Awaitable[CommandResult]:
return self._result(result)
@staticmethod
- async def _await_result(result: Awaitable[Any]) -> CommandResult:
+ async def _await_result(result: Awaitable[Any]) -> Result[str, str]:
return Command._result(await result)
@staticmethod
- def _result(result: Any) -> CommandResult:
+ def _result(result: Any) -> Result[str, str]:
+ if isinstance(result, (Ok, Err)):
+ return result
if result is None:
- return CommandResult(True, "")
+ return Ok("")
if isinstance(result, (tuple, list)):
if len(result) > 1:
- return CommandResult(bool(result[0]), str(result[1]))
+ text = str(result[1])
+ return Ok(text) if bool(result[0]) else Err(text)
if len(result) == 1:
result = result[0]
- return CommandResult(bool(result), "")
+ return Ok("") if bool(result) else Err("")
def __repr__(self) -> str:
if self.valid:
@@ -272,7 +270,7 @@ class PreparedCommand:
@dataclass(slots=True)
class _PendingCommand:
- task: asyncio.Future[CommandResult]
+ task: asyncio.Future[Result[str, str]]
name: str
argstring: str
command: Command
@@ -562,14 +560,16 @@ def _prepend_commands(self, commands: list[tuple[str, bytes | None]]) -> None:
self.cmdstack[0:0] = commands
def _echo_command_result(
- self, command_obj: Command, argstring: str, result: CommandResult
+ self, command_obj: Command, argstring: str, result: Result[str, str]
) -> None:
- success, text = result
- if not success:
- if not argstring:
- text = text or command_obj.brieftext()
- else:
- text = f"Error: {text or command_obj.brieftext()}"
+ match result:
+ case Ok(text):
+ pass
+ case Err(text):
+ if not argstring:
+ text = text or command_obj.brieftext()
+ else:
+ text = f"Error: {text or command_obj.brieftext()}"
if text:
self.console.echo(text)
@@ -658,7 +658,7 @@ def readscn(self, scn: str | Path | StringIO) -> Iterator[tuple[float, str]]:
if not (len(line.strip()) > 0 and line.strip()[0] == "#"):
self.console.echo(f"Skipping invalid scenario line: {line.strip()}")
- def ic(self, scn: str) -> tuple[bool, str]:
+ def ic(self, scn: str) -> Result[str, str]:
"""IC: Load a scenario file.
Resets the simulation, reads the scenario file, and buffers its
@@ -667,16 +667,13 @@ def ic(self, scn: str) -> tuple[bool, str]:
Args:
scn: The filename of the scenario, relative to the project root.
-
- Returns:
- tuple: (success (bool), message (str)).
"""
self.simulation.reset()
scn_path = self.scenario_root / scn
if not scn_path.exists():
- return False, f"IC: File not found: {scn_path}"
+ return Err(f"IC: File not found: {scn_path}")
lines = self.readscn(scn_path)
@@ -685,9 +682,9 @@ def ic(self, scn: str) -> tuple[bool, str]:
self.scencmd.append(cmd)
self.scenname = scn_path.stem
- return True, f"scenario {scn_path} loaded."
+ return Ok(f"scenario {scn_path} loaded.")
- def ic_StringIO(self, scn: StringIO, scn_name: str | None = None) -> tuple[bool, str]:
+ def ic_StringIO(self, scn: StringIO, scn_name: str | None = None) -> Result[str, str]:
"""IC: Load a scenario from a StringIO object.
Resets the simulation, reads scenario lines from the StringIO object,
@@ -696,9 +693,6 @@ def ic_StringIO(self, scn: StringIO, scn_name: str | None = None) -> tuple[bool,
Args:
scn: StringIO object containing scenario lines.
scn_name: The name of the scenario (optional).
-
- Returns:
- tuple: (success (bool), message (str)).
"""
# reset sim always
@@ -711,19 +705,16 @@ def ic_StringIO(self, scn: StringIO, scn_name: str | None = None) -> tuple[bool,
self.scencmd.append(cmd)
self.scenname = scn_name or ""
- return True, f"scenario {scn_name} loaded."
+ return Ok(f"scenario {scn_name} loaded.")
- def scenario(self, name: String) -> tuple[bool, str]:
+ def scenario(self, name: String) -> Result[str, str]:
"""SCENARIO: Set the scenario name for the current simulation.
Args:
name: The name to give the scenario.
-
- Returns:
- tuple: (True, confirmation message).
"""
self.scenname = name
- return True, "Starting scenario " + name
+ return Ok("Starting scenario " + name)
def schedule(self, time: Time, cmdline: String) -> bool:
"""SCHEDULE: Schedule a stack command at a specific simulation time.
@@ -765,7 +756,7 @@ def delay(self, time: Time, cmdline: String) -> bool:
self.scencmd.insert(idx, cmdline)
return True
- def showhelp(self, cmd: Txt = "", subcmd: Txt = "") -> tuple[bool, str]:
+ def showhelp(self, cmd: Txt = "", subcmd: Txt = "") -> Result[str, str]:
"""HELP: Display general help text or help text for a specific command,
or dump command reference in file when command is >filename.
@@ -774,15 +765,12 @@ def showhelp(self, cmd: Txt = "", subcmd: Txt = "") -> tuple[bool, str]:
tab-delimited command reference for all commands to a file
in the docs directory.
subcmd: Optional subcommand to display help for.
-
- Returns:
- tuple: (success (bool), help text or status message (str)).
"""
# Check if help is asked for a specific command
cmdobj = self.cmddict.get(cmd or "HELP")
if cmdobj:
- return True, cmdobj.helptext(subcmd)
+ return Ok(cmdobj.helptext(subcmd))
# Write command reference to tab-delimited text file
if cmd[0] == ">":
@@ -807,9 +795,9 @@ def showhelp(self, cmd: Txt = "", subcmd: Txt = "") -> tuple[bool, str]:
# Header of first table
f.write("Command\tDescription\tUsage\tArgument types\tFunction\tSynonyms\n")
f.write("\n".join(table))
- return True, "Writing command reference in " + fname
+ return Ok("Writing command reference in " + fname)
- return False, "HELP: Unknown command: " + cmd
+ return Err("HELP: Unknown command: " + cmd)
def checkscen(self) -> None:
"""Check if commands from the scenario buffer need to be stacked.
diff --git a/packages/minisky/minisky/stack/argparser.py b/packages/minisky/minisky/stack/argparser.py
index 4b59d7b..c0fa071 100644
--- a/packages/minisky/minisky/stack/argparser.py
+++ b/packages/minisky/minisky/stack/argparser.py
@@ -23,6 +23,7 @@
from types import SimpleNamespace, UnionType
from typing import TYPE_CHECKING, Annotated, Any, Union, get_args, get_origin
+from minisky.result import Err, Ok
from minisky.tools.convert import (
txt2alt,
txt2bool,
@@ -261,7 +262,11 @@ def parse(self, argstring: str) -> tuple:
callsign = arg.upper()
traffic = self.argument_parser.traffic
if callsign in traffic.groups:
- idx = traffic.groups.listgroup(callsign)
+ match traffic.groups.listgroup(callsign):
+ case Ok(group):
+ idx = group
+ case Err(error):
+ raise ArgumentError(error)
else:
idx = traffic.idx(callsign)
if idx < 0:
diff --git a/packages/minisky/minisky/tools/areafilter.py b/packages/minisky/minisky/tools/areafilter.py
index 3c71cd4..61b331f 100644
--- a/packages/minisky/minisky/tools/areafilter.py
+++ b/packages/minisky/minisky/tools/areafilter.py
@@ -17,6 +17,8 @@
import numpy as np
from matplotlib.path import Path
+from minisky.result import Err, Ok, Result
+
try:
from rtree.index import Index # type: ignore[assignment]
except (ImportError, OSError):
@@ -94,7 +96,7 @@ def define_area(
coordinates: tuple[float, ...] | list[float],
top: float = 1e9,
bottom: float = -1e9,
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""Define a new area, or list/inspect existing areas.
Args:
@@ -105,20 +107,17 @@ def define_area(
about the existing area with the given name is returned.
top: Top altitude bound [m] (default: effectively unbounded).
bottom: Bottom altitude bound [m] (default: effectively unbounded).
-
- Returns:
- tuple: (success (bool), message (str)).
"""
if areaname == "LIST":
if not self.basic_shapes:
- return True, "No shapes are currently defined."
+ return Ok("No shapes are currently defined.")
else:
- return True, "Currently defined shapes:\n" + ", ".join(self.basic_shapes)
+ return Ok("Currently defined shapes:\n" + ", ".join(self.basic_shapes))
if not coordinates:
if areaname in self.basic_shapes:
- return True, str(self.basic_shapes[areaname])
+ return Ok(str(self.basic_shapes[areaname]))
else:
- return False, f"Unknown shape: {areaname}"
+ return Err(f"Unknown shape: {areaname}")
old_shape = self.basic_shapes.get(areaname)
if old_shape is not None:
@@ -133,12 +132,12 @@ def define_area(
elif areatype == "LINE":
shape = Line(self, areaname, coordinates)
else:
- return False, f"Unknown shape type: {areatype}"
+ return Err(f"Unknown shape type: {areatype}")
self.basic_shapes[areaname] = shape
- return True, f"Created {areatype} {areaname}"
+ return Ok(f"Created {areatype} {areaname}")
- def define_box_area(self, name: str, *coords: float) -> tuple[bool, str]:
+ def define_box_area(self, name: str, *coords: float) -> Result[str, str]:
"""BOX: Define a box-shaped area.
Args:
@@ -148,7 +147,7 @@ def define_box_area(self, name: str, *coords: float) -> tuple[bool, str]:
"""
return self.define_area(name, "BOX", coords[:4], *coords[4:])
- def define_circle_area(self, name: str, *coords: float) -> tuple[bool, str]:
+ def define_circle_area(self, name: str, *coords: float) -> Result[str, str]:
"""CIRCLE: Define a circle-shaped area.
Args:
@@ -158,7 +157,7 @@ def define_circle_area(self, name: str, *coords: float) -> tuple[bool, str]:
"""
return self.define_area(name, "CIRCLE", coords[:3], *coords[3:])
- def define_line_area(self, name: str, *coords: float) -> tuple[bool, str]:
+ def define_line_area(self, name: str, *coords: float) -> Result[str, str]:
"""LINE: Draw a line between two positions on the radar screen.
Args:
@@ -167,7 +166,7 @@ def define_line_area(self, name: str, *coords: float) -> tuple[bool, str]:
"""
return self.define_area(name, "LINE", coords)
- def define_poly_area(self, name: str, *coords: float) -> tuple[bool, str]:
+ def define_poly_area(self, name: str, *coords: float) -> Result[str, str]:
"""POLY: Define a polygon-shaped area.
Args:
@@ -178,7 +177,7 @@ def define_poly_area(self, name: str, *coords: float) -> tuple[bool, str]:
def define_polyalt_area(
self, name: str, top: float, bottom: float, *coords: float
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""POLYALT: Define a polygon-shaped area in 3D, between two altitudes.
Args:
@@ -189,7 +188,7 @@ def define_polyalt_area(
"""
return self.define_area(name, "POLYALT", coords, top, bottom)
- def define_polyline_area(self, name: str, *coords: float) -> tuple[bool, str]:
+ def define_polyline_area(self, name: str, *coords: float) -> Result[str, str]:
"""POLYLINE: Draw a multi-segment line on the radar screen.
Args:
@@ -228,20 +227,17 @@ def reset(self) -> None:
self.areatree = Index()
self.max_area_id = 0
- def deleteArea(self, name: str) -> tuple[bool, str]:
+ def deleteArea(self, name: str) -> Result[str, str]:
"""Delete a previously defined area by name.
Args:
name: Name of the area shape to remove.
-
- Returns:
- tuple: (success (bool), message (str)).
"""
shape = self.basic_shapes.pop(name, None)
if shape is not None:
self._unregister(shape)
- return True, f"Area {name} deleted."
- return False, f"No area found with name {name}."
+ return Ok(f"Area {name} deleted.")
+ return Err(f"No area found with name {name}.")
def get_intersecting(self, lat0: float, lon0: float, lat1: float, lon1: float) -> list[Shape]:
"""Return all shapes that intersect with a specified rectangular area.
diff --git a/packages/minisky/minisky/tools/geo.py b/packages/minisky/minisky/tools/geo.py
index 2f56b8d..564b8c3 100644
--- a/packages/minisky/minisky/tools/geo.py
+++ b/packages/minisky/minisky/tools/geo.py
@@ -20,6 +20,7 @@
import pandas as pd
from minisky.core.config import data
+from minisky.result import Ok, Result
# Type alias for values that may be a scalar or a numpy array
FloatOrArray = float | np.ndarray
@@ -710,23 +711,19 @@ def load_magnetic_declination() -> np.ndarray:
# Command MAGVAR to get magnetic variation at position lat,lon
-def magdeccmd(latdeg: float, londeg: float) -> tuple[bool, str]:
+def magdeccmd(latdeg: float, londeg: float) -> Result[str, str]:
"""MAGVAR Get magnetic variation at position lat/lon.
Args:
latdeg: Latitude [deg].
londeg: Longitude [deg].
-
- Returns:
- tuple: (True, message stating the magnetic variation [deg]).
"""
- return (
- True,
+ return Ok(
"Magnetic variation at "
+ str(latdeg)
+ ","
+ str(londeg)
+ " = "
+ str(magdec(latdeg, londeg))
- + " deg",
+ + " deg"
)
diff --git a/packages/minisky/minisky/tools/navdata.py b/packages/minisky/minisky/tools/navdata.py
index 24f1d2d..5661759 100644
--- a/packages/minisky/minisky/tools/navdata.py
+++ b/packages/minisky/minisky/tools/navdata.py
@@ -17,6 +17,7 @@
import numpy as np
import pandas as pd
+from minisky.result import Err, Ok, Result
from minisky.tools import geo
from minisky.tools.aero import nm
@@ -184,7 +185,7 @@ def defwpt(
lat: float | None = None,
lon: float | None = None,
wptype: str | None = None,
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""DEFWPT: Define, inspect, or delete a scenario-specific waypoint.
Without lat/lon, information about the existing waypoint is
@@ -198,15 +199,12 @@ def defwpt(
lon: Longitude [deg].
wptype: Optional waypoint type (e.g. FIX, VOR, DME, NDB), or
DEL/DELETE to remove the waypoint.
-
- Returns:
- tuple: (success (bool), message (str)).
"""
# Prevent polluting the database: check arguments
if name == None or name == "":
- return False, "Insufficient arguments"
+ return Err("Insufficient arguments")
elif name.isdigit():
- return False, "Name needs to start with an alphabetical character"
+ return Err("Name needs to start with an alphabetical character")
# DEL command: give info on waypoint (shudl work wit or without lat,lon, may be clicked by accident
elif (wptype != None and (wptype.upper() == "DEL" or wptype.upper() == "DELETE")) or (
@@ -222,11 +220,11 @@ def defwpt(
txt = self.wpid[i] + " : " + str(self.wplat[i]) + "," + str(self.wplon[i])
if len(self.wptype[i]) > 0:
txt = txt + " " + self.wptype[i]
- return True, txt
+ return Ok(txt)
# Waypoint name is free
else:
- return True, "Waypoint " + name.upper() + " does not yet exist."
+ return Ok("Waypoint " + name.upper() + " does not yet exist.")
# Still here? So there is data, then we add this waypoint
self.wpid.append(name.upper())
@@ -246,24 +244,21 @@ def defwpt(
# Update screen info
self.console.addnavwpt(name.upper(), lat, lon)
- return True, name.upper() + " added to navdb."
+ return Ok(name.upper() + " added to navdb.")
- def delwpt(self, name: str | None = None) -> tuple[bool, str]:
+ def delwpt(self, name: str | None = None) -> Result[str, str]:
"""Delete a waypoint from the database.
The last-added occurrence of the name is removed.
Args:
name: Waypoint name.
-
- Returns:
- tuple: (success (bool), message (str)).
"""
if name is None:
- return False, "No waypoint name given"
+ return Err("No waypoint name given")
if self.wpid.count(name.upper()) <= 0:
- return False, "Waypoint " + name.upper() + " does not exist."
+ return Err("Waypoint " + name.upper() + " does not exist.")
idx = len(self.wpid) - self.wpid[::-1].index(name.upper()) - 1 # Search from back of list
@@ -281,7 +276,7 @@ def delwpt(self, name: str | None = None) -> tuple[bool, str]:
# Update screen info 9delete necessary there?)
self.console.removenavwpt(name.upper())
- return True, name.upper() + " deleted from navdb."
+ return Ok(name.upper() + " deleted from navdb.")
def getwpidx(self, txt: str, reflat: float = 999999.0, reflon: float = 999999) -> int:
"""Get waypoint index to access data.
diff --git a/packages/minisky/minisky/tools/position.py b/packages/minisky/minisky/tools/position.py
index 7231033..55a8d1e 100644
--- a/packages/minisky/minisky/tools/position.py
+++ b/packages/minisky/minisky/tools/position.py
@@ -10,6 +10,8 @@
from typing import TYPE_CHECKING
+from minisky.result import Err, Ok, Result
+
from .convert import txt2lat, txt2lon
if TYPE_CHECKING:
@@ -23,7 +25,7 @@ def txt2pos(
reflon: float,
navigation: Navdatabase,
traffic: Traffic,
-) -> tuple[bool, Position | str]:
+) -> Result[Position, str]:
"""Parse a position text into a Position object.
Args:
@@ -31,14 +33,11 @@ def txt2pos(
(e.g. "EHAM/RW06"), or aircraft callsign.
reflat: Reference latitude [deg], used to resolve ambiguous names.
reflon: Reference longitude [deg], used to resolve ambiguous names.
-
- Returns:
- tuple: (True, Position) on success, or (False, error message).
"""
pos = Position(name.upper().strip(), reflat, reflon, navigation, traffic)
if not pos.error:
- return True, pos
- return False, name + " not found in database"
+ return Ok(pos)
+ return Err(name + " not found in database")
def islat(txt: str) -> bool:
diff --git a/packages/minisky/minisky/traffic/asas/detection.py b/packages/minisky/minisky/traffic/asas/detection.py
index 6c22120..d39af2b 100644
--- a/packages/minisky/minisky/traffic/asas/detection.py
+++ b/packages/minisky/minisky/traffic/asas/detection.py
@@ -27,6 +27,7 @@
from minisky.core.config import MiniSkyConfig
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Ok, Result
from minisky.stack.argparser import Time, Txt
from minisky.tools.aero import ft, nm
@@ -205,7 +206,7 @@ def reset(self) -> None:
self.global_rpz = self.global_hpz = True
self.global_dtlook = self.global_dtnolook = True
- def switch(self, name: Txt = "ON") -> tuple | None:
+ def switch(self, name: Txt = "ON") -> Result[str, str]:
"""Turn Conflict Detection (CD) ON / OFF.
Switching off also clears the current conflict database.
@@ -213,9 +214,6 @@ def switch(self, name: Txt = "ON") -> tuple | None:
Args:
name (str): Either "ON" or "OFF".
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
-
Raises:
AssertionError: If `name` is not "ON" or "OFF".
"""
@@ -224,13 +222,12 @@ def switch(self, name: Txt = "ON") -> tuple | None:
if name == "OFF":
self.clearconfdb()
self.activate = False
- return True, "Conflict Detection turned off."
+ return Ok("Conflict Detection turned off.")
- if name == "ON":
- self.activate = True
- return True, "Conflict Detection is on."
+ self.activate = True
+ return Ok("Conflict Detection is on.")
- def setrpz(self, radius: float = -1.0, *acidx: int) -> tuple:
+ def setrpz(self, radius: float = -1.0, *acidx: int) -> Result[str, str]:
"""Set the horizontal separation distance (i.e., the radius of the
protected zone) in nautical miles.
@@ -244,20 +241,16 @@ def setrpz(self, radius: float = -1.0, *acidx: int) -> tuple:
*acidx: Aircraft index/indices or group. When not provided, the
default PZ radius is changed. Otherwise the PZ radius for the
passed aircraft is changed.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if radius < 0.0:
- return (
- True,
- f"ZONER [radius(nm), acid(s)/ac group]\nCurrent default PZ radius: {self.rpz_def / nm:.2f} NM",
+ return Ok(
+ f"ZONER [radius(nm), acid(s)/ac group]\nCurrent default PZ radius: {self.rpz_def / nm:.2f} NM"
)
if len(acidx) > 0:
idx: Any = acidx[0] if isinstance(acidx[0], np.ndarray) else acidx
self.rpz[idx] = radius * nm
self.global_rpz = False
- return True, f"Setting PZ radius to {radius} NM for {len(idx)} aircraft"
+ return Ok(f"Setting PZ radius to {radius} NM for {len(idx)} aircraft")
oldradius = self.rpz_def
self.rpz_def = radius * nm
if self.global_rpz:
@@ -265,9 +258,9 @@ def setrpz(self, radius: float = -1.0, *acidx: int) -> tuple:
# Adjust factors for reso zone if those were set with an absolute value
if not self.traffic.cr.resorrelative:
self.stack_command(f"RSZONER {self.traffic.cr.resofach * oldradius / nm}")
- return True, f"Setting default PZ radius to {radius} NM"
+ return Ok(f"Setting default PZ radius to {radius} NM")
- def sethpz(self, height: float = -1.0, *acidx: int) -> tuple:
+ def sethpz(self, height: float = -1.0, *acidx: int) -> Result[str, str]:
"""Set the vertical separation distance (i.e., half of the protected
zone height) in feet.
@@ -281,20 +274,16 @@ def sethpz(self, height: float = -1.0, *acidx: int) -> tuple:
*acidx: Aircraft index/indices or group. When not provided, the
default PZ height is changed. Otherwise the PZ height for the
passed aircraft is changed.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if height < 0.0:
- return (
- True,
- f"ZONEDH [height (ft), acid(s)/ac group]\nCurrent default PZ height: {self.hpz_def / ft:.2f} ft",
+ return Ok(
+ f"ZONEDH [height (ft), acid(s)/ac group]\nCurrent default PZ height: {self.hpz_def / ft:.2f} ft"
)
if len(acidx) > 0:
idx: Any = acidx[0] if isinstance(acidx[0], np.ndarray) else acidx
self.hpz[idx] = height * ft
self.global_hpz = False
- return True, f"Setting PZ height to {height} ft for {len(idx)} aircraft"
+ return Ok(f"Setting PZ height to {height} ft for {len(idx)} aircraft")
oldhpz = self.hpz_def
self.hpz_def = height * ft
if self.global_hpz:
@@ -302,9 +291,9 @@ def sethpz(self, height: float = -1.0, *acidx: int) -> tuple:
# Adjust factors for reso zone if those were set with an absolute value
if not self.traffic.cr.resodhrelative:
self.stack_command(f"RSZONEDH {self.traffic.cr.resofacv * oldhpz / ft}")
- return True, f"Setting default PZ height to {height} ft"
+ return Ok(f"Setting default PZ height to {height} ft")
- def setdtlook(self, time: Time = -1.0, *acidx: int) -> tuple:
+ def setdtlook(self, time: Time = -1.0, *acidx: int) -> Result[str, str]:
"""Set the lookahead time (in [hh:mm:]sec) for conflict detection.
Implements the DTLOOK stack command.
@@ -315,23 +304,20 @@ def setdtlook(self, time: Time = -1.0, *acidx: int) -> tuple:
*acidx: Aircraft index/indices or group. When not provided, the
default lookahead time is changed. Otherwise the lookahead
time for the passed aircraft is changed.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if time < 0.0:
- return True, f"DTLOOK[time]\nCurrent value: {self.dtlookahead_def: .1f} sec"
+ return Ok(f"DTLOOK[time]\nCurrent value: {self.dtlookahead_def: .1f} sec")
if len(acidx) > 0:
idx: Any = acidx[0] if isinstance(acidx[0], np.ndarray) else acidx
self.dtlookahead[idx] = time
self.global_dtlook = False
- return True, f"Setting CD lookahead to {time} sec for {len(idx)} aircraft"
+ return Ok(f"Setting CD lookahead to {time} sec for {len(idx)} aircraft")
self.dtlookahead_def = time
if self.global_dtlook:
self.dtlookahead[:] = time
- return True, f"Setting default CD lookahead to {time} sec"
+ return Ok(f"Setting default CD lookahead to {time} sec")
- def setdtnolook(self, time: Time = -1.0, *acidx: int) -> tuple:
+ def setdtnolook(self, time: Time = -1.0, *acidx: int) -> Result[str, str]:
"""Set the interval (in [hh:mm:]sec) in which conflict detection
is skipped after a conflict resolution.
@@ -343,21 +329,18 @@ def setdtnolook(self, time: Time = -1.0, *acidx: int) -> tuple:
*acidx: Aircraft index/indices or group. When not provided, the
default interval is changed. Otherwise the interval for the
passed aircraft is changed.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if time < 0.0:
- return True, f"DTNOLOOK[time]\nCurrent value: {self.dtnolook_def: .1f} sec"
+ return Ok(f"DTNOLOOK[time]\nCurrent value: {self.dtnolook_def: .1f} sec")
if len(acidx) > 0:
idx: Any = acidx[0] if isinstance(acidx[0], np.ndarray) else acidx
self.dtnolook[idx] = time
self.global_dtnolook = False
- return True, f"Setting CD no-look to {time} sec for {len(idx)} aircraft"
+ return Ok(f"Setting CD no-look to {time} sec for {len(idx)} aircraft")
self.dtnolook_def = time
if self.global_dtnolook:
self.dtnolook[:] = time
- return True, f"Setting default CD no-look to {time} sec"
+ return Ok(f"Setting default CD no-look to {time} sec")
def update(self, ownship: Any, intruder: Any) -> None:
"""Perform an update step of the Conflict Detection implementation.
diff --git a/packages/minisky/minisky/traffic/asas/mvp.py b/packages/minisky/minisky/traffic/asas/mvp.py
index 5dfc934..d36993b 100644
--- a/packages/minisky/minisky/traffic/asas/mvp.py
+++ b/packages/minisky/minisky/traffic/asas/mvp.py
@@ -22,6 +22,7 @@
import numpy as np
from minisky.core.config import MiniSkyConfig
+from minisky.result import Err, Ok, Result
from minisky.stack.argparser import Txt
from minisky.traffic.asas import ConflictResolution
@@ -55,7 +56,7 @@ def __init__(
self,
config: MiniSkyConfig,
traffic: Traffic,
- select_implementation: Callable[[str, str], tuple[bool, str]],
+ select_implementation: Callable[[str, str], Result[str, str]],
) -> None:
super().__init__(config, traffic, select_implementation)
# [-] switch to limit resolution to the horizontal direction
@@ -67,7 +68,7 @@ def __init__(
# [-] switch to limit resolution to the vertical direction
self.swresovert = False
- def setprio(self, flag=None, priocode="") -> bool | tuple:
+ def setprio(self, flag=None, priocode="") -> Result[str, str]:
"""Set the prio switch and the type of prio.
Implements the PRIORULES stack command for MVP. Validates the
@@ -77,13 +78,9 @@ def setprio(self, flag=None, priocode="") -> bool | tuple:
flag (bool): True to enable priority rules, False to disable.
When None, the available priority codes are reported.
priocode (str): One of "FF1", "FF2", "FF3", "LAY1", "LAY2".
-
- Returns:
- True on success, or (success (bool), message (str)) tuple.
"""
if flag is None:
- return (
- True,
+ return Ok(
"PRIORULES [ON/OFF] [PRIOCODE]"
+ "\nAvailable priority codes: "
+ "\n FF1: Free Flight Primary (No Prio) "
@@ -94,14 +91,14 @@ def setprio(self, flag=None, priocode="") -> bool | tuple:
+ "\nPriority is currently "
+ ("ON" if self.swprio else "OFF")
+ "\nPriority code is currently: "
- + str(self.priocode),
+ + str(self.priocode)
)
options = ["FF1", "FF2", "FF3", "LAY1", "LAY2"]
if priocode not in options:
- return False, "Priority code Not Understood. Available Options: " + str(options)
+ return Err("Priority code Not Understood. Available Options: " + str(options))
return super().setprio(flag, priocode)
- def setresometh(self, value: Txt = "") -> tuple:
+ def setresometh(self, value: Txt = "") -> Result[str, str]:
"""Processes the RMETHH command. Sets swresovert = False.
Selects which horizontal degrees of freedom MVP may use for
@@ -111,28 +108,21 @@ def setresometh(self, value: Txt = "") -> tuple:
Args:
value (str): One of "BOTH", "SPD", "HDG", "NONE", "ON", "OFF",
"OF". When empty, the current settings are reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
# Acceptable arguments for this command
options = ["BOTH", "SPD", "HDG", "NONE", "ON", "OFF", "OF"]
if not value:
- return (
- True,
+ return Ok(
"RMETHH [ON / BOTH / OFF / NONE / SPD / HDG]"
+ "\nHorizontal resolution limitation is currently "
+ ("ON" if self.swresohoriz else "OFF")
+ "\nSpeed resolution limitation is currently "
+ ("ON" if self.swresospd else "OFF")
+ "\nHeading resolution limitation is currently "
- + ("ON" if self.swresohdg else "OFF"),
+ + ("ON" if self.swresohdg else "OFF")
)
if value not in options:
- return (
- False,
- "RMETH Not Understood" + "\nRMETHH [ON / BOTH / OFF / NONE / SPD / HDG]",
- )
+ return Err("RMETH Not Understood" + "\nRMETHH [ON / BOTH / OFF / NONE / SPD / HDG]")
else:
if value == "ON" or value == "BOTH":
self.swresohoriz = True
@@ -154,9 +144,9 @@ def setresometh(self, value: Txt = "") -> tuple:
self.swresospd = False
self.swresohdg = True
self.swresovert = False
- return True, f"Horizontal resolution method set to {value}"
+ return Ok(f"Horizontal resolution method set to {value}")
- def setresometv(self, value: Txt = "") -> tuple:
+ def setresometv(self, value: Txt = "") -> Result[str, str]:
"""Processes the RMETHV command. Sets swresohoriz = False.
Enables (ON/"V/S") or disables (OFF/NONE) vertical-speed-only
@@ -165,24 +155,17 @@ def setresometv(self, value: Txt = "") -> tuple:
Args:
value (str): One of "ON", "V/S", "OFF", "OF", "NONE". When empty,
the current setting is reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
# Acceptable arguments for this command
options = ["NONE", "ON", "OFF", "OF", "V/S"]
if not value:
- return (
- True,
+ return Ok(
"RMETHV [ON / V/S / OFF / NONE]"
+ "\nVertical resolution limitation is currently "
- + ("ON" if self.swresovert else "OFF"),
+ + ("ON" if self.swresovert else "OFF")
)
if value not in options:
- return (
- False,
- f"RMETHV '{value}' Not Understood\nRMETHV [ON / V/S / OFF / NONE]",
- )
+ return Err(f"RMETHV '{value}' Not Understood\nRMETHV [ON / V/S / OFF / NONE]")
if value == "ON" or value == "V/S":
self.swresovert = True
@@ -192,7 +175,7 @@ def setresometv(self, value: Txt = "") -> tuple:
elif value == "OFF" or value == "OF" or value == "NONE":
# Do NOT swtich off self.swresohoriz if value == OFF
self.swresovert = False
- return True, f"Vertical resolution method set to {value}"
+ return Ok(f"Vertical resolution method set to {value}")
def applyprio(
self,
diff --git a/packages/minisky/minisky/traffic/asas/resolution.py b/packages/minisky/minisky/traffic/asas/resolution.py
index e6e2b8e..d33b071 100644
--- a/packages/minisky/minisky/traffic/asas/resolution.py
+++ b/packages/minisky/minisky/traffic/asas/resolution.py
@@ -22,6 +22,7 @@
from minisky.core.config import MiniSkyConfig
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Err, Ok, Result
from minisky.stack.argparser import Txt
from minisky.tools.aero import ft, nm
from minisky.traffic import route
@@ -71,7 +72,7 @@ def __init__(
self,
config: MiniSkyConfig,
traffic: Traffic,
- select_implementation: Callable[[str, str], tuple[bool, str]],
+ select_implementation: Callable[[str, str], Result[str, str]],
) -> None:
super().__init__()
self.config = config
@@ -322,7 +323,7 @@ def anglediff(a: float, b: float) -> float:
# Remove pairs from the list that are past CPA or have deleted aircraft
self.resopairs -= delpairs
- def setprio(self, flag: bool | None = None, priocode="") -> bool | tuple:
+ def setprio(self, flag: bool | None = None, priocode="") -> Result[str, str]:
"""Define priority rules (right of way) for conflict resolution.
Implements the PRIORULES stack command. The base class only stores
@@ -333,23 +334,19 @@ def setprio(self, flag: bool | None = None, priocode="") -> bool | tuple:
flag (bool): True to enable priority rules, False to disable.
When None, an informational message is returned.
priocode (str): Identifier of the priority rule set to use.
-
- Returns:
- True on success, or (False, message) when not applicable.
"""
if flag is None:
if self.__class__ is ConflictResolution:
- return False, "No conflict resolution enabled."
- return (
- False,
- f"Resolution algorithm {self.__class__.__name__} hasn't implemented priority.",
+ return Err("No conflict resolution enabled.")
+ return Err(
+ f"Resolution algorithm {self.__class__.__name__} hasn't implemented priority."
)
self.swprio = flag
self.priocode = priocode
- return True
+ return Ok("")
- def setnoreso(self, *idx: int) -> bool | tuple:
+ def setnoreso(self, *idx: int) -> Result[str, str]:
"""ADD or Remove aircraft that nobody will avoid.
Multiple aircraft can be sent to this function at once.
@@ -360,22 +357,18 @@ def setnoreso(self, *idx: int) -> bool | tuple:
Args:
*idx: Aircraft indices to toggle. When empty, the current list of
flagged aircraft is reported.
-
- Returns:
- True on success, or (True, message) when reporting.
"""
if not idx:
- return (
- True,
+ return Ok(
"NORESO [ACID, ... ] OR NORESO [GROUPID]"
+ "\nCurrent list of aircraft nobody will avoid:"
- + ", ".join(np.array(self.traffic.callsign)[self.noresoac]),
+ + ", ".join(np.array(self.traffic.callsign)[self.noresoac])
)
indices = list(idx)
self.noresoac[indices] = np.logical_not(self.noresoac[indices])
- return True
+ return Ok("")
- def setresooff(self, *idx: int) -> bool | tuple:
+ def setresooff(self, *idx: int) -> Result[str, str]:
"""ADD or Remove aircraft that will not avoid anybody else.
Multiple aircraft can be sent to this function at once.
@@ -386,23 +379,19 @@ def setresooff(self, *idx: int) -> bool | tuple:
Args:
*idx: Aircraft indices to toggle. When empty, the current list of
flagged aircraft is reported.
-
- Returns:
- True on success, or (True, message) when reporting.
"""
if not idx:
- return (
- True,
+ return Ok(
"RESOOFF [ACID, ... ] OR RESOOFF [GROUPID]"
+ "\nCurrent list of aircraft will not avoid anybody:"
- + ", ".join(np.array(self.traffic.callsign)[self.resooffac]),
+ + ", ".join(np.array(self.traffic.callsign)[self.resooffac])
)
else:
indices = list(idx)
self.resooffac[indices] = np.logical_not(self.resooffac[indices])
- return True
+ return Ok("")
- def setresofach(self, factor: float | None = None) -> tuple:
+ def setresofach(self, factor: float | None = None) -> Result[str, str]:
"""Set resolution factor horizontal
(to maneuver only a fraction of a resolution vector).
@@ -414,23 +403,17 @@ def setresofach(self, factor: float | None = None) -> tuple:
Args:
factor (float): Horizontal resolution factor [-]. When None, the
current factor is reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if factor is None:
- return (
- True,
- f"RFACH [FACTOR]\nCurrent horizontal resolution factor is: {self.resofach}",
- )
+ return Ok(f"RFACH [FACTOR]\nCurrent horizontal resolution factor is: {self.resofach}")
else:
self.resofach = factor
self.resorrelative = (
True # Size of resolution zone r, vertically, set relative to CD zone
)
- return True, f"Horizontal resolution factor set to {self.resofach}"
+ return Ok(f"Horizontal resolution factor set to {self.resofach}")
- def setresofacv(self, factor: float | None = None) -> tuple:
+ def setresofacv(self, factor: float | None = None) -> Result[str, str]:
"""Set resolution factor vertical (to maneuver only a fraction of a resolution vector).
Implements the RFACV stack command. The vertical resolution zone
@@ -439,21 +422,15 @@ def setresofacv(self, factor: float | None = None) -> tuple:
Args:
factor (float): Vertical resolution factor [-]. When None, the
current factor is reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if factor is None:
- return (
- True,
- f"RFACV [FACTOR]\nCurrent vertical resolution factor is: {self.resofacv}",
- )
+ return Ok(f"RFACV [FACTOR]\nCurrent vertical resolution factor is: {self.resofacv}")
self.resofacv = factor
# Size of resolution zone dh, vertically, set relative to CD zone
self.resodhrelative = True
- return True, f"Vertical resolution factor set to {self.resofacv}"
+ return Ok(f"Vertical resolution factor set to {self.resofacv}")
- def setresozoner(self, zoner: float | None = None) -> tuple:
+ def setresozoner(self, zoner: float | None = None) -> Result[str, str]:
"""Set resolution factor horizontal, but then with absolute value
(to maneuver only a fraction of a resolution vector).
@@ -465,31 +442,25 @@ def setresozoner(self, zoner: float | None = None) -> tuple:
Args:
zoner (float): Resolution zone radius [NM]. When None, the current
factor and resulting radius are reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if not self.traffic.cd.global_rpz:
self.resorrelative = True
- return (
- False,
- "RSZONER [radiusnm]\nCan only set resolution factor when simulation contains aircraft with different RPZ,\nUse RFACH instead.",
+ return Err(
+ "RSZONER [radiusnm]\nCan only set resolution factor when simulation contains aircraft with different RPZ,\nUse RFACH instead."
)
if zoner is None:
- return (
- True,
- f"RSZONER [radiusnm]\nCurrent horizontal resolution factor is: {self.resofach}, resulting in radius: {self.resofach * self.traffic.cd.rpz_def / nm} nm",
+ return Ok(
+ f"RSZONER [radiusnm]\nCurrent horizontal resolution factor is: {self.resofach}, resulting in radius: {self.resofach * self.traffic.cd.rpz_def / nm} nm"
)
self.resofach = zoner / self.traffic.cd.rpz_def * nm
# Size of resolution zone r, vertically, no longer relative to CD zone
self.resorrelative = False
- return (
- True,
- f"Horizontal resolution factor updated to {self.resofach}, resulting in radius: {zoner} nm",
+ return Ok(
+ f"Horizontal resolution factor updated to {self.resofach}, resulting in radius: {zoner} nm"
)
- def setresozonedh(self, zonedh: float | None = None) -> tuple:
+ def setresozonedh(self, zonedh: float | None = None) -> Result[str, str]:
"""Set resolution factor vertical (to maneuver only a fraction of a
resolution vector), but then with absolute value.
@@ -501,31 +472,25 @@ def setresozonedh(self, zonedh: float | None = None) -> tuple:
Args:
zonedh (float): Resolution zone height [ft]. When None, the
current factor and resulting height are reported.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
if not self.traffic.cd.global_hpz:
self.resodhrelative = True
- return (
- False,
- "RSZONEH [zonedhft]\nCan only set resolution factor when simulation contains aircraft with different HPZ,\nUse RFACV instead.",
+ return Err(
+ "RSZONEH [zonedhft]\nCan only set resolution factor when simulation contains aircraft with different HPZ,\nUse RFACV instead."
)
if zonedh is None:
- return (
- True,
- f"RSZONEDH [zonedhft]\nCurrent vertical resolution factor is: {self.resofacv}, resulting in height: {self.resofacv * self.traffic.cd.hpz_def / ft} ft",
+ return Ok(
+ f"RSZONEDH [zonedhft]\nCurrent vertical resolution factor is: {self.resofacv}, resulting in height: {self.resofacv * self.traffic.cd.hpz_def / ft} ft"
)
self.resofacv = zonedh / self.traffic.cd.hpz_def * ft
# Size of resolution zone dh, vertically, no longer relative to CD zone
self.resodhrelative = False
- return (
- True,
- f"Vertical resolution factor updated to {self.resofacv}, resulting in height: {zonedh} ft",
+ return Ok(
+ f"Vertical resolution factor updated to {self.resofacv}, resulting in height: {zonedh} ft"
)
- def setmethod(self, name: Txt = "") -> tuple:
+ def setmethod(self, name: Txt = "") -> Result[str, str]:
"""Select a Conflict Resolution method.
Implements the RESO stack command. Selecting "MVP" replaces the
@@ -534,37 +499,34 @@ def setmethod(self, name: Txt = "") -> tuple:
Args:
name (str): "OFF", "MVP", or empty to report available methods.
-
- Returns:
- tuple: (success (bool), message (str)) for the command stack.
"""
names = ["OFF", "MVP"]
if not name:
curname = type(self.traffic.cr).__name__ if self.traffic.cr.activate else "OFF"
- return (
- True,
- f"Current CR method: {curname}" + f"\nAvailable CR methods: {', '.join(names)}",
+ return Ok(
+ f"Current CR method: {curname}" + f"\nAvailable CR methods: {', '.join(names)}"
)
if name == "OFF":
self.traffic.cr.switch(False)
- return True, "Conflict Resolution turned off."
+ return Ok("Conflict Resolution turned off.")
if name == "MVP":
- success, message = self.select_implementation("CONFLICTRESOLUTION", name)
- if not success:
- return success, message
- self.traffic.cr.switch(True)
- return True, "Selected MVP as Conflict Resolution method."
+ match self.select_implementation("CONFLICTRESOLUTION", name):
+ case Err() as error:
+ return error
+ case Ok():
+ self.traffic.cr.switch(True)
+ return Ok("Selected MVP as Conflict Resolution method.")
- return False, f"Unknown method: {name}. Available: {', '.join(names)}"
+ return Err(f"Unknown method: {name}. Available: {', '.join(names)}")
- def setresometh(self, value: Txt = "") -> tuple:
+ def setresometh(self, value: Txt = "") -> Result[str, str]:
"""Report that horizontal method selection requires the MVP implementation."""
- return False, f"RMETHH is not available for CR method {type(self).__name__}"
+ return Err(f"RMETHH is not available for CR method {type(self).__name__}")
- def setresometv(self, value: Txt = "") -> tuple:
+ def setresometv(self, value: Txt = "") -> Result[str, str]:
"""Report that vertical method selection requires the MVP implementation."""
- return False, f"RMETHV is not available for CR method {type(self).__name__}"
+ return Err(f"RMETHV is not available for CR method {type(self).__name__}")
diff --git a/packages/minisky/minisky/traffic/autopilot.py b/packages/minisky/minisky/traffic/autopilot.py
index 36b61da..af19b1a 100644
--- a/packages/minisky/minisky/traffic/autopilot.py
+++ b/packages/minisky/minisky/traffic/autopilot.py
@@ -23,6 +23,7 @@
import numpy as np
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Err, Ok, Result
from minisky.stack.argparser import Acid, Alt, Hdg, OnOff, Spd, Vspd, Wpt
from minisky.tools import geo
from minisky.tools.aero import (
@@ -36,7 +37,7 @@
vcasormach2tas,
)
from minisky.tools.convert import degto180
-from minisky.tools.position import Position, txt2pos
+from minisky.tools.position import txt2pos
from .route import Route, direct
@@ -933,7 +934,7 @@ def setspeedforRTA(self, idx: int, torta: Any, xtorta: float) -> float | bool:
def selaltcmd(
self, idx: int | np.ndarray, alt: Alt, vspd: Vspd | None = None
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""Select the autopilot altitude, optionally with a vertical speed.
Implements the ALT stack command: `ALT acid, alt, [vspd]`.
@@ -946,9 +947,6 @@ def selaltcmd(
idx: Aircraft index (or collection of indices).
alt: Selected altitude [m] (stack input in ft/FL).
vspd: Optional vertical speed [m/s] (stack input in fpm).
-
- Returns:
- tuple: (True, confirmation message).
"""
self.traffic.selalt[idx] = alt
self.traffic.swvnav[idx] = False
@@ -967,9 +965,9 @@ def selaltcmd(
)
self.traffic.selvs[idxarr[oppositevs]] = 0.0
- return True, f"altitude set to {alt / ft} ft"
+ return Ok(f"altitude set to {alt / ft} ft")
- def selvspdcmd(self, idx: int, vspd: Vspd) -> tuple[bool, str]:
+ def selvspdcmd(self, idx: int, vspd: Vspd) -> Result[str, str]:
"""Select the autopilot vertical speed.
Implements the VS stack command: `VS acid, vspd (ft/min)`.
@@ -978,15 +976,12 @@ def selvspdcmd(self, idx: int, vspd: Vspd) -> tuple[bool, str]:
Args:
idx: Aircraft index.
vspd: Selected vertical speed [m/s] (stack input in fpm).
-
- Returns:
- tuple: (True, confirmation message).
"""
self.traffic.selvs[idx] = vspd
self.traffic.swvnav[idx] = False
- return True, f"vertical speed set to {vspd / fpm} ft/min"
+ return Ok(f"vertical speed set to {vspd / fpm} ft/min")
- def selhdgcmd(self, idx: int, hdg: Hdg) -> tuple[bool, str]: # HDG command
+ def selhdgcmd(self, idx: int, hdg: Hdg) -> Result[str, str]: # HDG command
"""Select the autopilot heading.
Implements the HDG stack command: `HDG acid, hdg (deg)`. When a
@@ -998,9 +993,6 @@ def selhdgcmd(self, idx: int, hdg: Hdg) -> tuple[bool, str]: # HDG command
Args:
idx: Aircraft index.
hdg: Selected heading [deg].
-
- Returns:
- tuple: (True, confirmation message).
"""
if self.traffic.wind.winddim > 0:
@@ -1021,9 +1013,9 @@ def selhdgcmd(self, idx: int, hdg: Hdg) -> tuple[bool, str]: # HDG command
self.trk[idx] = hdg
self.traffic.swlnav[idx] = False
- return True, f"heading set to {hdg} deg"
+ return Ok(f"heading set to {hdg} deg")
- def selspdcmd(self, idx: int, casmach: Spd) -> tuple[bool, str]: # SPD command
+ def selspdcmd(self, idx: int, casmach: Spd) -> Result[str, str]: # SPD command
"""Select the autopilot speed.
Implements the SPD stack command: `SPD acid, casmach`. Switches
@@ -1035,9 +1027,6 @@ def selspdcmd(self, idx: int, casmach: Spd) -> tuple[bool, str]: # SPD command
idx: Aircraft index.
casmach: Selected speed: CAS [m/s] or Mach [-] (values above 1.0
are interpreted as CAS; stack input in kts or Mach).
-
- Returns:
- tuple: (True, confirmation message).
"""
# Depending on or position relative to crossover altitude,
# we will maintain CAS or Mach when altitude changes
@@ -1052,11 +1041,11 @@ def selspdcmd(self, idx: int, casmach: Spd) -> tuple[bool, str]: # SPD command
else:
msg = f"speed set to Mach {casmach}"
- return True, msg
+ return Ok(msg)
def setdest(
self, acidx: Acid, wpname: Wpt | None = None, casmach: Spd | None = None
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""Set (or show) the destination of an aircraft.
Implements the DEST stack command: `DEST acid, latlon/airport`.
@@ -1071,12 +1060,9 @@ def setdest(
current destination is reported.
casmach: Optional speed constraint at the destination, CAS [m/s]
or Mach [-].
-
- Returns:
- tuple: (success flag, message).
"""
if wpname is None:
- return True, "DEST " + self.traffic.callsign[acidx] + ": " + self.dest[acidx]
+ return Ok("DEST " + self.traffic.callsign[acidx] + ": " + self.dest[acidx])
route = self.route[acidx]
@@ -1089,19 +1075,18 @@ def setdest(
reflat = self.traffic.lat[acidx]
reflon = self.traffic.lon[acidx]
- success, posobj = txt2pos(
+ match txt2pos(
wpname,
float(reflat),
float(reflon),
self.navigation,
self.traffic,
- )
- if success:
- assert isinstance(posobj, Position)
- lat = posobj.lat
- lon = posobj.lon
- else:
- return False, "DEST: Position " + wpname + " not found."
+ ):
+ case Ok(posobj):
+ lat = posobj.lat
+ lon = posobj.lon
+ case Err():
+ return Err("DEST: Position " + wpname + " not found.")
else:
lat = self.navigation.aptlat[apidx]
@@ -1126,11 +1111,11 @@ def setdest(
# If not found, say so
elif iwp < 0:
- return False, ("DEST position" + self.dest[acidx] + " not found.")
+ return Err("DEST position" + self.dest[acidx] + " not found.")
- return True, f"destination set to {wpname}"
+ return Ok(f"destination set to {wpname}")
- def setorig(self, acidx: int, wpname: Wpt | None = None) -> tuple[bool, str]:
+ def setorig(self, acidx: int, wpname: Wpt | None = None) -> Result[str, str]:
"""Set (or show) the origin of an aircraft.
Implements the ORIG stack command: `ORIG acid, latlon/airport`.
@@ -1141,12 +1126,9 @@ def setorig(self, acidx: int, wpname: Wpt | None = None) -> tuple[bool, str]:
acidx: Aircraft index.
wpname: Airport identifier or position text; when omitted, the
current origin is reported.
-
- Returns:
- tuple: (success flag, message).
"""
if wpname is None:
- return True, "ORIG " + self.traffic.callsign[acidx] + ": " + self.orig[acidx]
+ return Ok("ORIG " + self.traffic.callsign[acidx] + ": " + self.orig[acidx])
route = self.route[acidx]
@@ -1160,19 +1142,18 @@ def setorig(self, acidx: int, wpname: Wpt | None = None) -> tuple[bool, str]:
reflat = self.traffic.lat[acidx]
reflon = self.traffic.lon[acidx]
- success, posobj = txt2pos(
+ match txt2pos(
wpname,
float(reflat),
float(reflon),
self.navigation,
self.traffic,
- )
- if success:
- assert isinstance(posobj, Position)
- lat = posobj.lat
- lon = posobj.lon
- else:
- return False, ("ORIG: Position " + wpname + " not found.")
+ ):
+ case Ok(posobj):
+ lat = posobj.lat
+ lon = posobj.lon
+ case Err():
+ return Err("ORIG: Position " + wpname + " not found.")
else:
lat = self.navigation.aptlat[apidx]
@@ -1184,11 +1165,11 @@ def setorig(self, acidx: int, wpname: Wpt | None = None) -> tuple[bool, str]:
acidx, self.orig[acidx], route.orig, lat, lon, 0.0, self.traffic.cas[acidx]
)
if iwp < 0:
- return False, (self.orig[acidx] + " not found.")
+ return Err(self.orig[acidx] + " not found.")
- return True, f"origin set to {wpname}"
+ return Ok(f"origin set to {wpname}")
- def setVNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
+ def setVNAV(self, idx: Any, flag: OnOff | None = None) -> Result[str, str]:
"""Switch VNAV (vertical FMS guidance) on or off, or show its state.
Implements the VNAV stack command: `VNAV acid, [ON/OFF]`. VNAV can
@@ -1200,9 +1181,6 @@ def setVNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
idx: Aircraft index, collection of indices, or None for all
aircraft.
flag: True/False to switch on/off; None to report the state.
-
- Returns:
- tuple: (success flag, status message).
"""
if not isinstance(idx, Collection):
if idx is None:
@@ -1229,7 +1207,7 @@ def setVNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
elif flag:
if not self.traffic.swlnav[i]:
- return False, (self.traffic.callsign[i] + ": VNAV ON requires LNAV to be ON")
+ return Err(self.traffic.callsign[i] + ": VNAV ON requires LNAV to be ON")
route = self.route[i]
if len(route.wpname) > 0:
@@ -1247,7 +1225,7 @@ def setVNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
self.traffic.actwp.nextaltco[i] = self.route[i].wptoalt[actwpidx]
else:
- return False, (
+ return Err(
"VNAV "
+ self.traffic.callsign[i]
+ ": no waypoints or destination specified"
@@ -1256,11 +1234,11 @@ def setVNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
self.traffic.swvnav[i] = False
self.traffic.swvnavspd[i] = False
if flag == None:
- return True, "\n".join(output)
+ return Ok("\n".join(output))
- return True, f"VNAV {'ON' if flag else 'OFF'}"
+ return Ok(f"VNAV {'ON' if flag else 'OFF'}")
- def setLNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
+ def setLNAV(self, idx: Any, flag: OnOff | None = None) -> Result[str, str]:
"""Switch LNAV (lateral FMS guidance) on or off, or show its state.
Implements the LNAV stack command: `LNAV acid, [ON/OFF]`. LNAV can
@@ -1272,9 +1250,6 @@ def setLNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
idx: Aircraft index, collection of indices, or None for all
aircraft.
flag: True/False to switch on/off; None to report the state.
-
- Returns:
- tuple: (success flag, status message).
"""
if not isinstance(idx, Collection):
if idx is None:
@@ -1298,7 +1273,7 @@ def setLNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
elif flag:
route = self.route[i]
if len(route.wpname) <= 0:
- return False, (
+ return Err(
"LNAV "
+ self.traffic.callsign[i]
+ ": no waypoints or destination specified"
@@ -1309,11 +1284,11 @@ def setLNAV(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
else:
self.traffic.swlnav[i] = False
if flag is None:
- return True, "\n".join(output)
+ return Ok("\n".join(output))
- return True, f"LNAV {'ON' if flag else 'OFF'}"
+ return Ok(f"LNAV {'ON' if flag else 'OFF'}")
- def setswtoc(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
+ def setswtoc(self, idx: Any, flag: OnOff | None = None) -> Result[str, str]:
"""Switch the Top-of-Climb logic on or off, or show its state.
Implements the SWTOC stack command: `SWTOC acid, [ON/OFF]`. With
@@ -1325,9 +1300,6 @@ def setswtoc(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
idx: Aircraft index, collection of indices, or None for all
aircraft.
flag: True/False to switch on/off; None to report the state.
-
- Returns:
- tuple: (True, status message).
"""
if not isinstance(idx, Collection):
@@ -1352,11 +1324,11 @@ def setswtoc(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
else:
self.swtoc[i] = False
if flag is None:
- return True, "\n".join(output)
+ return Ok("\n".join(output))
- return True, f"SWTOC {'ON' if flag else 'OFF'}"
+ return Ok(f"SWTOC {'ON' if flag else 'OFF'}")
- def setswtod(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
+ def setswtod(self, idx: Any, flag: OnOff | None = None) -> Result[str, str]:
"""Switch the Top-of-Descent logic on or off, or show its state.
Implements the SWTOD stack command: `SWTOD acid, [ON/OFF]`. With
@@ -1368,9 +1340,6 @@ def setswtod(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
idx: Aircraft index, collection of indices, or None for all
aircraft.
flag: True/False to switch on/off; None to report the state.
-
- Returns:
- tuple: (True, status message).
"""
if not isinstance(idx, Collection):
if idx is None:
@@ -1394,9 +1363,9 @@ def setswtod(self, idx: Any, flag: OnOff | None = None) -> tuple[bool, str]:
else:
self.swtod[i] = False
if flag is None:
- return True, "\n".join(output)
+ return Ok("\n".join(output))
- return True, f"SWTOD {'ON' if flag else 'OFF'}"
+ return Ok(f"SWTOD {'ON' if flag else 'OFF'}")
def calcvrta(v0: float, dx: float, deltime: float, trafax: float) -> float:
diff --git a/packages/minisky/minisky/traffic/performance/perfoap.py b/packages/minisky/minisky/traffic/performance/perfoap.py
index ff91318..9c3bceb 100644
--- a/packages/minisky/minisky/traffic/performance/perfoap.py
+++ b/packages/minisky/minisky/traffic/performance/perfoap.py
@@ -15,6 +15,7 @@
import numpy as np
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Ok, Result
from minisky.tools import aero
from minisky.tools.aero import fpm, ft, kts
@@ -522,7 +523,7 @@ def calc_axmax(self) -> np.ndarray:
return axmax
- def show_performance(self, acid: int) -> tuple:
+ def show_performance(self, acid: int) -> Result[str, str]:
"""Report the current performance state of one aircraft.
Implements the PERFSTATS stack command output: flight phase, thrust,
@@ -531,19 +532,13 @@ def show_performance(self, acid: int) -> tuple:
Args:
acid (int): Aircraft index.
-
- Returns:
- tuple: (True, message (str)) for the command stack.
"""
- return (
- True,
- (
- f"Flight phase: {ph.readable_phase(self.phase[acid])}\n"
- f"Thrust: {self.thrust[acid] / 1000:.0f} kN\n"
- f"Drag: {self.drag[acid] / 1000:.0f} kN\n"
- f"Fuel flow: {self.fuelflow[acid]:.2f} kg/s\n"
- f"Speed envelope: [{self.vmin[acid] / kts:.0f}, {self.vmax[acid] / kts:.0f}] kts\n"
- f"Vertical speed envelope: [{self.vsmin[acid] / fpm:.0f}, {self.vsmax[acid] / fpm:.0f}] fpm\n"
- f"Ceiling: {self.hmax[acid] / ft:.0f} ft"
- ),
+ return Ok(
+ f"Flight phase: {ph.readable_phase(self.phase[acid])}\n"
+ f"Thrust: {self.thrust[acid] / 1000:.0f} kN\n"
+ f"Drag: {self.drag[acid] / 1000:.0f} kN\n"
+ f"Fuel flow: {self.fuelflow[acid]:.2f} kg/s\n"
+ f"Speed envelope: [{self.vmin[acid] / kts:.0f}, {self.vmax[acid] / kts:.0f}] kts\n"
+ f"Vertical speed envelope: [{self.vsmin[acid] / fpm:.0f}, {self.vsmax[acid] / fpm:.0f}] fpm\n"
+ f"Ceiling: {self.hmax[acid] / ft:.0f} ft"
)
diff --git a/packages/minisky/minisky/traffic/route.py b/packages/minisky/minisky/traffic/route.py
index c6400aa..82001f9 100644
--- a/packages/minisky/minisky/traffic/route.py
+++ b/packages/minisky/minisky/traffic/route.py
@@ -20,12 +20,14 @@
import numpy as np
+from minisky.result import Err, Ok, Result
+
# from minisky.core import Replaceable
from minisky.stack.argparser import Alt, Spd, Time, Wpt
from minisky.tools import geo
from minisky.tools.aero import casormach2tas, ft, g0, kts, mach2cas, nm
from minisky.tools.convert import degto180, txt2alt, txt2spd
-from minisky.tools.position import Position, txt2pos
+from minisky.tools.position import txt2pos
if TYPE_CHECKING:
from minisky.traffic import Traffic
@@ -857,7 +859,9 @@ def change_wpt_mode(traffic: Traffic, acidx: int, mode=None, value=None) -> bool
return True
-def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all arguments of addwpt
+def addwpt(
+ traffic: Traffic, ac: str | int, *args
+) -> Result[str, str]: # args: all arguments of addwpt
"""Add a waypoint to the route of an aircraft.
Implements the ADDWPT stack command:
@@ -880,9 +884,6 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
Args:
ac: Aircraft callsign or index.
*args: Remaining ADDWPT arguments as described above.
-
- Returns:
- bool or tuple: True on success, or (success flag, message).
"""
# First get the appropriate ac route
@@ -903,17 +904,17 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
if swwpmode == "FLYBY":
acrte.swflyby = True
acrte.swflyturn = False
- return True
+ return Ok("")
elif swwpmode == "FLYOVER":
acrte.swflyby = False
acrte.swflyturn = False
- return True
+ return Ok("")
elif swwpmode == "FLYTURN":
acrte.swflyby = False
acrte.swflyturn = True
- return True
+ return Ok("")
elif len(args) == 2:
swwpmode = args[0].replace("-", "")
@@ -925,13 +926,13 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
else:
acrte.turnrad = float(args[1] / ft * nm) # arg was originally parsed as wpalt
except (TypeError, ValueError):
- return False, "Error in processing value of turn radius"
+ return Err("Error in processing value of turn radius")
# Switch flyturn automatically when this is set
acrte.swflyby = False
acrte.swflyturn = True
- return True
+ return Ok("")
elif swwpmode == "TURNSPD" or swwpmode == "TURNSPEED":
try:
@@ -942,7 +943,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
args[1] * kts / ft
) # [m/s] Arg was wpalt Keep it as IAS/CAS orig in kts, now in m/s
except (TypeError, ValueError):
- return False, "Error in processing value of turn speed"
+ return Err("Error in processing value of turn speed")
# Switch flyturn automatically when this is set
acrte.swflyby = False
@@ -955,13 +956,13 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
else:
acrte.turnhdgr = args[1] / ft # [deg/s] turn rate
except (TypeError, ValueError):
- return False, "Error in processing value of turn heading rate"
+ return Err("Error in processing value of turn heading rate")
# Switch flyturn automatically when this is set
acrte.swflyby = False
acrte.swflyturn = True
- return True
+ return Ok("")
# Convert to positions
name = args[0].upper().strip()
@@ -995,36 +996,35 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
# Normal waypoint (no take-off waypoint => see else)
if not takeoffwpt:
# Get waypoint position
- success, posobj = txt2pos(name, reflat, reflon, traffic.navigation, traffic)
- if success:
- assert isinstance(posobj, Position)
- lat = posobj.lat
- lon = posobj.lon
+ match txt2pos(name, reflat, reflon, traffic.navigation, traffic):
+ case Ok(posobj):
+ lat = posobj.lat
+ lon = posobj.lon
- if posobj.type == "nav" or posobj.type == "apt":
- wptype = Route.wpnav
+ if posobj.type == "nav" or posobj.type == "apt":
+ wptype = Route.wpnav
- elif posobj.type == "rwy":
- wptype = Route.runway
+ elif posobj.type == "rwy":
+ wptype = Route.runway
- else: # treat as lat/lon
- name = callsign
- wptype = Route.wplatlon
+ else: # treat as lat/lon
+ name = callsign
+ wptype = Route.wplatlon
- if len(args) > 1 and args[1]:
- alt = args[1]
+ if len(args) > 1 and args[1]:
+ alt = args[1]
- if len(args) > 2 and args[2]:
- spd = args[2]
+ if len(args) > 2 and args[2]:
+ spd = args[2]
- if len(args) > 3 and args[3]:
- afterwp = args[3]
+ if len(args) > 3 and args[3]:
+ afterwp = args[3]
- if len(args) > 4 and args[4]:
- beforewp = args[4]
+ if len(args) > 4 and args[4]:
+ beforewp = args[4]
- else:
- return False, "Waypoint " + name + " not found."
+ case Err():
+ return Err("Waypoint " + name + " not found.")
# Take off waypoint: positioned 20% of the runway length after the runway
else:
@@ -1077,24 +1077,23 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
try:
rwyhdg = float(rwydir) * 10.0
except ValueError:
- return False, name + " not found."
+ return Err(name + " not found.")
- success, posobj = txt2pos(
+ match txt2pos(
aptid + "/RW" + rwyid,
reflat,
reflon,
traffic.navigation,
traffic,
- )
- if success:
- assert isinstance(posobj, Position)
- rwylat, rwylon = posobj.lat, posobj.lon
- else:
- rwylat = traffic.lat[acidx]
- rwylon = traffic.lon[acidx]
+ ):
+ case Ok(posobj):
+ rwylat, rwylon = posobj.lat, posobj.lon
+ case Err():
+ rwylat = traffic.lat[acidx]
+ rwylon = traffic.lon[acidx]
else:
- return False, "Use ADDWPT TAKEOFF,AIRPORTID,RWYNAME"
+ return Err("Use ADDWPT TAKEOFF,AIRPORTID,RWYNAME")
# Create a waypoint 2 nm away from current point
rwydist = 2.0 # [nm] use default distance away from threshold
@@ -1122,7 +1121,7 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
# Check for success by checking inserted location in flight plan >= 0
if wpidx < 0:
- return False, "Waypoint " + name + " not added."
+ return Err("Waypoint " + name + " not added.")
# check for presence of orig/dest
norig = int(traffic.ap.orig[acidx] != "") # 1 if orig is present in route
@@ -1136,12 +1135,9 @@ def addwpt(traffic: Traffic, ac: str | int, *args) -> bool | tuple: # args: all
traffic.swlnav[acidx] = True
if afterwp and acrte.wpname.count(afterwp) == 0:
- return (
- True,
- "Waypoint " + afterwp + " not found\n" + "waypoint added at end of route",
- )
+ return Ok("Waypoint " + afterwp + " not found\n" + "waypoint added at end of route")
else:
- return True
+ return Ok("")
def addwpt_before(
@@ -1152,7 +1148,7 @@ def addwpt_before(
waypoint,
alt: Alt | None = None,
spd: Spd | None = None,
-) -> bool | tuple:
+) -> Result[str, str]:
"""Add a waypoint to a route before an existing waypoint.
Implements the BEFORE stack command:
@@ -1166,9 +1162,6 @@ def addwpt_before(
waypoint: Waypoint name or lat/lon text of the new waypoint.
alt: Optional altitude constraint [m].
spd: Optional speed constraint, CAS [m/s] or Mach [-].
-
- Returns:
- bool or tuple: Result of addwpt().
"""
return addwpt(traffic, acidx, waypoint, alt, spd, None, beforewp)
@@ -1181,7 +1174,7 @@ def addwpt_after(
waypoint,
alt: Alt | None = None,
spd: Spd | None = None,
-) -> bool | tuple:
+) -> Result[str, str]:
"""Add a waypoint to a route after an existing waypoint.
Implements the AFTER stack command:
@@ -1195,14 +1188,11 @@ def addwpt_after(
waypoint: Waypoint name or lat/lon text of the new waypoint.
alt: Optional altitude constraint [m].
spd: Optional speed constraint, CAS [m/s] or Mach [-].
-
- Returns:
- bool or tuple: Result of addwpt().
"""
return addwpt(traffic, acidx, waypoint, alt, spd, afterwp)
-def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
+def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> Result[str, str]:
"""Show, set or delete constraints and commands at a route waypoint.
Implements the AT stack command:
@@ -1225,9 +1215,6 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
acidx: Aircraft index.
atwp: Name of the waypoint in the route.
*args: Remaining AT arguments as described above.
-
- Returns:
- bool or tuple: True on success, or (success flag, message).
"""
acid = traffic.callsign[acidx]
acrte = traffic.ap.route[acidx]
@@ -1293,7 +1280,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
for stackedtxt in acrte.wpstack[wpidx]:
txt = txt + stackedtxt + "\n"
- return True, txt
+ return Ok(txt)
elif args[0].count("/") == 1:
# Set both alt & speed at this waypoint
@@ -1323,7 +1310,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
success = False
if not success:
- return False, "Could not parse " + args[0] + " as alt / spd"
+ return Err("Could not parse " + args[0] + " as alt / spd")
# If success: update flight plan and guidance
acrte.calcfp()
@@ -1348,14 +1335,14 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
try:
acrte.wpalt[wpidx] = txt2alt(args[1])
except ValueError as e:
- return False, e.args[0]
+ return Err(e.args[0])
# Edit waypoint speed constraint
elif swspd:
try:
acrte.wpspd[wpidx] = txt2spd(args[1])
except ValueError as e:
- return False, e.args[0]
+ return Err(e.args[0])
# add stack command: args[1] is DO or STACK, args[2:] contains a command
elif swat:
@@ -1382,10 +1369,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
# This command does not need an acid or it is already first argument
acrte.wpstack[wpidx].append(" ".join(args[1:]))
except (AttributeError, IndexError, KeyError, TypeError):
- return (
- False,
- "Stacked command " + cmd + " unknown or syntax error",
- )
+ return Err("Stacked command " + cmd + " unknown or syntax error")
else:
# Command line starts with an aircraft id at the beginning of the command line, stack it
acrte.wpstack[wpidx].append(" ".join(args[1:]))
@@ -1407,7 +1391,7 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
acrte.wpstack[wpidx] = []
else:
- return False, "No " + args[0] + " at ", atwp
+ return Err(f"No {args[0]} at {atwp}")
# If success: update flight plan and guidance
acrte.calcfp()
@@ -1415,9 +1399,9 @@ def at_wpt(traffic: Traffic, acidx: int, atwp: Wpt, *args) -> bool | tuple:
# Waypoint not found in route
else:
- return False, atwp + " not found in route " + acid
+ return Err(atwp + " not found in route " + acid)
- return True
+ return Ok("")
def direct(traffic: Traffic, acidx: int, wpname: Wpt) -> bool:
@@ -1558,7 +1542,7 @@ def set_rta(
return True
-def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> tuple | None:
+def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> Result[None, str]:
"""Show the route of an aircraft in the console, page by page.
Implements the LISTRTE stack command: `LISTRTE acid, [pagenr]`.
@@ -1572,7 +1556,7 @@ def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> tuple | None:
ipagetxt: Page number as text (default "0").
Returns:
- tuple or None: (False, message) when the aircraft has no route.
+ Result: `Ok(None)` after listing the route, or `Err` when no route exists.
"""
# First get the appropriate ac route
ipage = int(ipagetxt)
@@ -1581,7 +1565,7 @@ def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> tuple | None:
n_wpt = len(acrte.wpname)
if n_wpt <= 0:
- return False, "Aircraft has no route."
+ return Err("Aircraft has no route.")
for i in range(ipage * 7, ipage * 7 + 7):
if 0 <= i < n_wpt:
@@ -1625,8 +1609,10 @@ def listrte(traffic: Traffic, acidx: int, ipagetxt: str = "0") -> tuple | None:
# Display message
traffic.console.echo(txt)
+ return Ok(None)
-def delrte(traffic: Traffic, acidx: int | None = None) -> bool | tuple:
+
+def delrte(traffic: Traffic, acidx: int | None = None) -> Result[str, str]:
"""Delete the complete route (including origin/destination) of an
aircraft.
@@ -1636,15 +1622,12 @@ def delrte(traffic: Traffic, acidx: int | None = None) -> bool | tuple:
Args:
acidx: Aircraft index; may be None when only one aircraft exists.
-
- Returns:
- bool or tuple: True on success, or (False, error message).
"""
if acidx is None:
if traffic.ntraf == 0:
- return False, "No aircraft in simulation"
+ return Err("No aircraft in simulation")
if traffic.ntraf > 1:
- return False, "Specify callsign of aircraft to delete route of"
+ return Err("Specify callsign of aircraft to delete route of")
acidx = 0
# Simple re-initialize this route as empty
acid = traffic.callsign[acidx]
@@ -1656,10 +1639,10 @@ def delrte(traffic: Traffic, acidx: int | None = None) -> bool | tuple:
traffic.swvnav[acidx] = False
traffic.swvnavspd[acidx] = False
- return True
+ return Ok("")
-def delwpt(traffic: Traffic, acidx: int, wpname: Wpt) -> bool | tuple:
+def delwpt(traffic: Traffic, acidx: int, wpname: Wpt) -> Result[str, str]:
"""Delete a single waypoint from the route of an aircraft.
Implements the DELWPT stack command: `DELWPT acid, wpname`. When the
@@ -1670,9 +1653,6 @@ def delwpt(traffic: Traffic, acidx: int, wpname: Wpt) -> bool | tuple:
Args:
acidx: Aircraft index.
wpname: Name of the waypoint to delete.
-
- Returns:
- bool or tuple: True on success, or (False, error message).
"""
# Look up waypoint
@@ -1682,7 +1662,7 @@ def delwpt(traffic: Traffic, acidx: int, wpname: Wpt) -> bool | tuple:
try:
wpidx = acrte.wpname.index(wpname.upper())
except ValueError:
- return False, "Waypoint " + wpname + " not found"
+ return Err("Waypoint " + wpname + " not found")
# check if active way point is the one being deleted and that it is not the last wpt.
# If active wpt is deleted then change path of aircraft
@@ -1716,4 +1696,4 @@ def delwpt(traffic: Traffic, acidx: int, wpname: Wpt) -> bool | tuple:
traffic.swvnav[acidx] = False
traffic.swvnavspd[acidx] = False
- return True
+ return Ok("")
diff --git a/packages/minisky/minisky/traffic/traffic.py b/packages/minisky/minisky/traffic/traffic.py
index 61d880f..7b9882d 100644
--- a/packages/minisky/minisky/traffic/traffic.py
+++ b/packages/minisky/minisky/traffic/traffic.py
@@ -22,6 +22,7 @@
from minisky.core.config import MiniSkyConfig
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Err, Ok, Result
from minisky.tools import geo
from minisky.tools.aero import (
DEFAULT_CASMACH_THRESHOLD,
@@ -142,7 +143,7 @@ def __init__(
get_simulation: Callable[[], Simulation],
stack_command: Callable[..., None],
get_command_registry: Callable[[], Mapping[str, object]],
- select_implementation: Callable[[str, str], tuple[bool, str]],
+ select_implementation: Callable[[str, str], Result[str, str]],
) -> None:
super().__init__()
self.config = config
@@ -247,24 +248,21 @@ def __init__(
# Default bank angles per flight phase
self.bphase = np.deg2rad(np.array([15, 35, 35, 35, 15, 45]))
- def casmachthr(self, threshold: float | None = None) -> tuple[bool, str]:
+ def casmachthr(self, threshold: float | None = None) -> Result[str, str]:
"""Get or set this runtime's CAS/Mach interpretation threshold.
Positive speed values below this threshold are interpreted as Mach
numbers by CRE, MOVE, route, and autopilot speed conversions.
"""
if threshold is None:
- return (
- True,
- (
- "CASMACHTHR: The current CAS/Mach threshold is "
- f"{self.casmach_threshold} m/s "
- f"({self.casmach_threshold / kts} kts)"
- ),
+ return Ok(
+ "CASMACHTHR: The current CAS/Mach threshold is "
+ f"{self.casmach_threshold} m/s "
+ f"({self.casmach_threshold / kts} kts)"
)
self.casmach_threshold = threshold
- return True, f"CASMACHTHR: Set CAS/Mach threshold to {threshold}"
+ return Ok(f"CASMACHTHR: Set CAS/Mach threshold to {threshold}")
@property
def command_registry(self) -> Mapping[str, object]:
@@ -314,7 +312,7 @@ def cre(
hdg: float = 45.0,
alt: float = 25000 * ft,
spd: float = 300 * kts,
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""Create a single aircraft and add it to the traffic database.
Implements the CRE stack command. After creation, any commands stored
@@ -331,13 +329,10 @@ def cre(
defaults to 25000 ft.
spd: Initial speed: CAS [m/s] or Mach [-] (stack input in kts);
defaults to 300 kts.
-
- Returns:
- tuple: (success flag, confirmation or error message).
"""
if callsign in self.callsign:
- return False, f"aircraft {callsign} already exists"
+ return Err(f"aircraft {callsign} already exists")
# covert to array with 1 element
acid_ = np.array([callsign.upper()])
@@ -350,7 +345,7 @@ def cre(
self.__create_aircraft(acid_, actype_, lat_, lon_, hdg_, alt_, spd_)
- return True, f"Aircraft {callsign} created"
+ return Ok(f"Aircraft {callsign} created")
def mcre(
self,
@@ -362,7 +357,7 @@ def mcre(
actype: str = "A320",
acalt: int | None = None,
acspd: int | None = None,
- ) -> tuple[bool, str]:
+ ) -> Result[str, str]:
"""Create multiple aircraft at random positions in a lat/lon box.
Implements the MCRE stack command. Callsigns are generated randomly
@@ -379,9 +374,6 @@ def mcre(
actype: ICAO aircraft type designator for all aircraft.
acalt: Optional fixed altitude [m]; random when None.
acspd: Optional fixed speed, CAS [m/s] or Mach; random when None.
-
- Returns:
- tuple: (True, confirmation message).
"""
# Generate random callsigns
@@ -409,7 +401,7 @@ def mcre(
self.__create_aircraft(np.array(callsign), actype_, aclat, aclon, achdg, acalt_, acspd_)
- return True, f"{n} aircraft created"
+ return Ok(f"{n} aircraft created")
def __create_aircraft(
self,
@@ -847,7 +839,7 @@ def idx(self, callsign: str | Iterable[str]) -> int | list:
except ValueError:
return -1
- def setnoise(self, noise: bool | None = None) -> bool | tuple[bool, str]:
+ def setnoise(self, noise: bool | None = None) -> Result[str, str]:
"""Switch trajectory noise models on or off, or report their state.
Implements the NOISE stack command. Controls both the turbulence
@@ -856,16 +848,13 @@ def setnoise(self, noise: bool | None = None) -> bool | tuple[bool, str]:
Args:
noise: True/False to enable/disable noise; None to report the
current state.
-
- Returns:
- bool or tuple: True on set, or (True, status message) on query.
"""
if noise is None:
- return True, "Noise is currently " + ("on" if self.turbulence.active else "off")
+ return Ok("Noise is currently " + ("on" if self.turbulence.active else "off"))
self.turbulence.setnoise(noise)
self.noise.setnoise(noise)
- return True
+ return Ok("")
def engchange(self, acid: int, engid: str) -> None:
"""Change the engine type of an aircraft in the performance model.
@@ -919,7 +908,7 @@ def move(
self.vs[idx] = vspd
self.swvnav[idx] = False
- def position(self, id_or_name: int | str) -> tuple[bool, str]:
+ def position(self, id_or_name: int | str) -> Result[str, str]:
"""Show information on an aircraft, airport, waypoint or navaid.
Implements the POS stack command. Dispatches to
@@ -931,9 +920,6 @@ def position(self, id_or_name: int | str) -> tuple[bool, str]:
Args:
id_or_name: Aircraft index (int) or the name of an aircraft,
airport, waypoint, navaid or airway (str).
-
- Returns:
- tuple: (success flag, multi-line information text).
"""
if isinstance(id_or_name, int):
@@ -941,7 +927,7 @@ def position(self, id_or_name: int | str) -> tuple[bool, str]:
else:
return self.position_by_name(id_or_name)
- def position_aircraft(self, idx: int) -> tuple[bool, str]:
+ def position_aircraft(self, idx: int) -> Result[str, str]:
"""Generate a position report for a single aircraft.
The report includes position, heading/track [deg], altitude [ft],
@@ -950,9 +936,6 @@ def position_aircraft(self, idx: int) -> tuple[bool, str]:
Args:
idx: Aircraft index.
-
- Returns:
- tuple: (True, multi-line position report).
"""
acid = self.callsign[idx]
@@ -999,9 +982,9 @@ def position_aircraft(self, idx: int) -> tuple[bool, str]:
if self.ap.dest[idx] != "":
info = info + " to " + self.ap.dest[idx]
- return True, info
+ return Ok(info)
- def position_by_name(self, name: str) -> tuple[bool, str]:
+ def position_by_name(self, name: str) -> Result[str, str]:
"""Look up a name and generate an information report for it.
Searches, in order: airports, aircraft callsigns, waypoints/navaids,
@@ -1011,9 +994,6 @@ def position_by_name(self, name: str) -> tuple[bool, str]:
Args:
name: Name/identifier to look up (case-insensitive).
-
- Returns:
- tuple: (success flag, multi-line information text).
"""
name = name.upper()
@@ -1046,7 +1026,7 @@ def position_by_name(self, name: str) -> tuple[bool, str]:
if runways:
lines += f"Runways: {', '.join(runways)}\n"
- return True, lines
+ return Ok(lines)
# try aircraft
idx_ac = self.idx(name)
@@ -1114,7 +1094,7 @@ def position_by_name(self, name: str) -> tuple[bool, str]:
lines += f"Connected to airways: {'-'.join(awset)}\n"
- return True, lines
+ return Ok(lines)
# Try airway id
else: # airway
@@ -1124,36 +1104,32 @@ def position_by_name(self, name: str) -> tuple[bool, str]:
lines = ""
for segment in airway:
lines += f"Airway {awid}: {' - '.join(segment)}\n"
- return True, lines
+ return Ok(lines)
# nothing matched
- return False, f"{name} not found as aircraft, airport, navaid, or waypoint"
+ return Err(f"{name} not found as aircraft, airport, navaid, or waypoint")
# Show what we found on airport and navaid/waypoint
- def settrans(self, alt: float = -999.0) -> bool | tuple[bool, str]:
+ def settrans(self, alt: float = -999.0) -> Result[str, str]:
"""Set or show the transition level.
Args:
alt: New transition level [m] (stack input in ft/FL). With the
default sentinel value the current level is reported instead.
-
- Returns:
- bool or tuple: True on set, (True, message) on query, or
- (False, error message) for invalid values.
"""
# in case a valid value is ginve set it
if alt > -900.0:
if alt > 0.0:
self.translvl = alt
- return True
- return False, "Transition level needs to be ft/FL and larger than zero"
+ return Ok("")
+ return Err("Transition level needs to be ft/FL and larger than zero")
# In case no value is given, show it
tlvl = round(self.translvl / ft)
- return True, f"Transition level = {tlvl}/FL{round(tlvl / 100.0)}"
+ return Ok(f"Transition level = {tlvl}/FL{round(tlvl / 100.0)}")
- def setbanklim(self, idx: int, bankangle: float | None = None) -> bool | tuple[bool, str]:
+ def setbanklim(self, idx: int, bankangle: float | None = None) -> Result[str, str]:
"""Set or show the bank angle limit for a given aircraft.
Implements the BANK stack command. The limit is used by the autopilot
@@ -1163,19 +1139,15 @@ def setbanklim(self, idx: int, bankangle: float | None = None) -> bool | tuple[b
idx: Aircraft index.
bankangle: New bank limit [deg]; when omitted, the current limit
is reported.
-
- Returns:
- bool or tuple: True on set, or (True, status message) on query.
"""
if bankangle:
self.ap.bankdef[idx] = np.radians(bankangle) # [rad]
- return True
- return (
- True,
- f"Banklimit of {self.callsign[idx]} is {int(np.degrees(self.ap.bankdef[idx]))} deg",
+ return Ok("")
+ return Ok(
+ f"Banklimit of {self.callsign[idx]} is {int(np.degrees(self.ap.bankdef[idx]))} deg"
)
- def setthrottle(self, idx: int, throttle: str = "") -> bool | tuple[bool, str]:
+ def setthrottle(self, idx: int, throttle: str = "") -> Result[str, str]:
"""Set the throttle of an aircraft, or report the autothrottle state.
Implements the THR stack command. "AUTO"/"OFF" re-engages the
@@ -1186,10 +1158,6 @@ def setthrottle(self, idx: int, throttle: str = "") -> bool | tuple[bool, str]:
Args:
idx: Aircraft index.
throttle: Throttle argument string; empty to query the state.
-
- Returns:
- bool or tuple: True on set, (True, status message) on query, or
- (False, error message) for invalid input.
"""
if throttle:
@@ -1213,26 +1181,23 @@ def setthrottle(self, idx: int, throttle: str = "") -> bool | tuple[bool, str]:
try:
x = factor * float(throttle)
except ValueError:
- return False, "THR invalid argument " + throttle
+ return Err("THR invalid argument " + throttle)
# Check whether value makes sense
if x < 0.0 or x > 1.0:
- return (
- False,
- "THR invalid value " + throttle + ". Needs to be [0.0 , 1.0]",
- )
+ return Err("THR invalid value " + throttle + ". Needs to be [0.0 , 1.0]")
# Valid value, set throttle and disable autothrottle
self.swats[idx] = False
self.thr[idx] = x
- return True
+ return Ok("")
if self.swats[idx]:
- return True, "ATS of " + self.callsign[idx] + " is ON"
- return True, "ATS of " + self.callsign[idx] + " is OFF. THR is " + str(self.thr[idx])
+ return Ok("ATS of " + self.callsign[idx] + " is ON")
+ return Ok("ATS of " + self.callsign[idx] + " is OFF. THR is " + str(self.thr[idx]))
- def crecmd(self, cmdline: str) -> tuple[bool, str]:
+ def crecmd(self, cmdline: str) -> Result[str, str]:
"""Add a command to the list issued for every newly created aircraft.
Implements the CRECMD stack command. Each stored command line is
@@ -1242,9 +1207,6 @@ def crecmd(self, cmdline: str) -> tuple[bool, str]:
Args:
cmdline: Command line (without callsign) to add to the list, or
""/"?" to show the current list.
-
- Returns:
- tuple: (True, message).
"""
# Help text need or info on current list?
if cmdline == "" or cmdline == "?":
@@ -1255,29 +1217,23 @@ def crecmd(self, cmdline: str) -> tuple[bool, str]:
allcmds = "[acid] " + txt
else:
allcmds += "; [acid] " + txt
- return True, "CRECMD list: " + allcmds
+ return Ok("CRECMD list: " + allcmds)
else:
- return (
- True,
- "CRECMD will add a/c specific commands to an aircraft after creation",
- )
+ return Ok("CRECMD will add a/c specific commands to an aircraft after creation")
# Command to be added to list
else:
self.crecmdlist.append(cmdline)
- return True, ""
+ return Ok("")
- def clrcrecmd(self) -> tuple[bool, str]:
+ def clrcrecmd(self) -> Result[str, str]:
"""Clear the list of commands issued for newly created aircraft.
Implements the CLRCRECMD stack command, removing all command lines
previously added with CRECMD.
-
- Returns:
- tuple: (True, message).
"""
ncrecmd = len(self.crecmdlist)
if ncrecmd == 0:
- return True, "CLRCRECMD deletes all commands on clears command"
+ return Ok("CLRCRECMD deletes all commands on clears command")
else:
self.crecmdlist = []
- return True, f"All {ncrecmd} crecmd commands deleted."
+ return Ok(f"All {ncrecmd} crecmd commands deleted.")
diff --git a/packages/minisky/minisky/traffic/trafficgroups.py b/packages/minisky/minisky/traffic/trafficgroups.py
index c174684..54853bc 100644
--- a/packages/minisky/minisky/traffic/trafficgroups.py
+++ b/packages/minisky/minisky/traffic/trafficgroups.py
@@ -17,6 +17,7 @@
import numpy as np
from minisky.core import TrafficArrays
+from minisky.result import Err, Ok, Result
if TYPE_CHECKING:
from minisky.tools.areafilter import AreaFilter
@@ -71,7 +72,7 @@ def __contains__(self, groupname: str) -> bool:
# Check if a group with a name exists
return groupname in self.groups or groupname == "*"
- def group(self, groupname: str = "", *args: Any) -> tuple[bool, str]:
+ def group(self, groupname: str = "", *args: Any) -> Result[str, str]:
"""Add aircraft to a group, list its members, or list all groups.
Implements the GROUP stack command. Without arguments the existing
@@ -83,21 +84,18 @@ def group(self, groupname: str = "", *args: Any) -> tuple[bool, str]:
Args:
groupname: Name of the group; empty to list all groups.
*args: Aircraft indices, or a single area name.
-
- Returns:
- tuple: (success flag, message).
"""
# Return list of groups if no groupname is given
if not groupname:
if not self.groups:
- return True, "There are currently no traffic groups defined."
+ return Ok("There are currently no traffic groups defined.")
else:
- return True, "Defined traffic groups:\n" + ", ".join(self.groups)
+ return Ok("Defined traffic groups:\n" + ", ".join(self.groups))
if len(self.groups) >= 64:
- return False, "Maximum number of 64 groups reached"
+ return Err("Maximum number of 64 groups reached")
if groupname not in self.groups:
if not args:
- return False, f"Group {groupname} doesn't exist"
+ return Err(f"Group {groupname} doesn't exist")
# Get first unused group mask
for i in range(64):
groupmask = 1 << i
@@ -107,8 +105,12 @@ def group(self, groupname: str = "", *args: Any) -> tuple[bool, str]:
break
elif not args:
- acnames = np.array(self.traffic.callsign)[self.listgroup(groupname)]
- return True, "Aircraft in group {}:\n{}".format(groupname, ", ".join(acnames))
+ match self.listgroup(groupname):
+ case Ok(group):
+ acnames = np.array(self.traffic.callsign)[group]
+ return Ok("Aircraft in group {}:\n{}".format(groupname, ", ".join(acnames)))
+ case Err(error):
+ return Err(error)
# Add aircraft to group
if self.areas.has_area(args[0]):
@@ -121,7 +123,7 @@ def group(self, groupname: str = "", *args: Any) -> tuple[bool, str]:
idx = list(args)
self.ingroup[idx] |= self.groups[groupname]
acnames = np.array(self.traffic.callsign)[idx]
- return True, "Aircraft added to group {}:\n{}".format(groupname, ", ".join(acnames))
+ return Ok("Aircraft added to group {}:\n{}".format(groupname, ", ".join(acnames)))
def delgroup(self, grouparray: Any) -> None:
"""Delete a group, and all aircraft in that group.
@@ -141,7 +143,7 @@ def delgroup(self, grouparray: Any) -> None:
if grouparray.groupname != "*":
self.allmasks ^= self.groups.pop(grouparray.groupname)
- def ungroup(self, groupname: str, *args: Any) -> tuple[bool, str] | None:
+ def ungroup(self, groupname: str, *args: Any) -> Result[None, str]:
"""Remove members from a group by aircraft index.
Implements the UNGROUP stack command.
@@ -151,15 +153,15 @@ def ungroup(self, groupname: str, *args: Any) -> tuple[bool, str] | None:
*args: Indices of the aircraft to remove from the group.
Returns:
- tuple or None: (False, error message) when the group does not
- exist.
+ Result: `Ok(None)` after removal, or `Err` when the group is unknown.
"""
groupmask = self.groups.get(groupname, None)
if groupmask is None:
- return False, f"Group {groupname} doesn't exist"
+ return Err(f"Group {groupname} doesn't exist")
self.ingroup[list(args)] ^= groupmask
+ return Ok(None)
- def listgroup(self, groupname: str) -> Any:
+ def listgroup(self, groupname: str) -> Result[GroupArray, str]:
"""Return the aircraft indices of all aircraft in a group.
When "*" is passed as group name, all aircraft in the simulation
@@ -167,15 +169,10 @@ def listgroup(self, groupname: str) -> Any:
Args:
groupname: Name of the group, or "*" for all aircraft.
-
- Returns:
- GroupArray: Indices of the group members (with the group name
- attached), or (False, error message) when the group does not
- exist.
"""
if groupname == "*":
- return GroupArray(range(self.traffic.ntraf), groupname="*")
+ return Ok(GroupArray(range(self.traffic.ntraf), groupname="*"))
groupmask = self.groups.get(groupname, None)
if groupmask is None:
- return False, f"Group {groupname} doesn't exist"
- return GroupArray(np.where((self.ingroup & groupmask) > 0)[0], groupname=groupname)
+ return Err(f"Group {groupname} doesn't exist")
+ return Ok(GroupArray(np.where((self.ingroup & groupmask) > 0)[0], groupname=groupname))
diff --git a/packages/minisky/minisky/traffic/trails.py b/packages/minisky/minisky/traffic/trails.py
index 2ffb933..53b7b75 100644
--- a/packages/minisky/minisky/traffic/trails.py
+++ b/packages/minisky/minisky/traffic/trails.py
@@ -14,6 +14,7 @@
import numpy as np
from minisky.core import TrafficArrays
+from minisky.result import Err, Ok, Result
if TYPE_CHECKING:
from minisky.simulation import Simulation
@@ -248,7 +249,7 @@ def clear(self) -> None:
self.clearbg()
self.clearnew()
- def setTrails(self, *args: Any) -> bool | tuple[bool, str]:
+ def setTrails(self, *args: Any) -> Result[str, str]:
"""Switch trails on/off, or change the trail color of an aircraft.
Implements the TRAIL stack command:
@@ -260,16 +261,13 @@ def setTrails(self, *args: Any) -> bool | tuple[bool, str]:
*args: Either a bool (on/off) optionally followed by the
segment time resolution [s], or an aircraft index followed
by a color name (BLUE/RED/YELLOW).
-
- Returns:
- bool or tuple: True on success, or (success flag, message).
"""
if len(args) == 0:
msg = "TRAIL ON/OFF, [dt] / TRAIL acid color\n"
msg = msg + "TRAILS ARE ON" if self.active else msg + "TRAILS ARE OFF"
- return True, msg
+ return Ok(msg)
# Switch on/off
elif type(args[0]) == bool:
@@ -284,13 +282,10 @@ def setTrails(self, *args: Any) -> bool | tuple[bool, str]:
else:
# Change trail color
if len(args) < 2 or args[1] not in ["BLUE", "RED", "YELLOW"]:
- return (
- False,
- "Set aircraft trail color with: TRAIL acid BLUE/RED/YELLOW",
- )
+ return Err("Set aircraft trail color with: TRAIL acid BLUE/RED/YELLOW")
self.changeTrailColor(args[1], args[0])
- return True
+ return Ok("")
def changeTrailColor(self, color: str, idx: int) -> None:
"""Change the trail color of one aircraft.
diff --git a/packages/minisky/minisky/traffic/wind.py b/packages/minisky/minisky/traffic/wind.py
index 11c4e46..d54c936 100644
--- a/packages/minisky/minisky/traffic/wind.py
+++ b/packages/minisky/minisky/traffic/wind.py
@@ -18,6 +18,7 @@
from scipy.interpolate import LinearNDInterpolator, interp1d
from minisky.core.trafficarrays import TrafficArrays
+from minisky.result import Err, Ok, Result
from minisky.stack.argparser import Alt, Lat, Lon
from minisky.tools.aero import ft, kts
@@ -412,7 +413,7 @@ class Wind(TrafficArrays, Windfield):
Available at runtime as [`runtime.traffic.wind`][minisky.traffic.wind.Wind].
"""
- def add(self, lat: Lat, lon: Lon, *winddata: float) -> bool | tuple[bool, str]:
+ def add(self, lat: Lat, lon: Lon, *winddata: float) -> Result[str, str]:
"""Define a wind vector as part of the 2D or 3D wind field.
Implements the WIND stack command.
@@ -443,7 +444,7 @@ def add(self, lat: Lat, lon: Lon, *winddata: float) -> bool | tuple[bool, str]:
# No altitude or just one: same wind for all altitudes at this position
elif ndata == 2 or (ndata == 3 and winddata[0] is None): # only one point, ignore altitude
if winddata[-2] is None or winddata[-1] is None:
- return False, "Wind direction and speed needed."
+ return Err("Wind direction and speed needed.")
self.addpoint(lat, lon, winddata[-2], winddata[-1] * kts)
@@ -457,11 +458,11 @@ def add(self, lat: Lat, lon: Lon, *winddata: float) -> bool | tuple[bool, str]:
self.addpoint(lat, lon, dirarr, spdarr, altarr)
else: # Something is wrong
- return False, "Winddata not recognized"
+ return Err("Winddata not recognized")
- return True
+ return Ok("")
- def get(self, lat: Lat, lon: Lon, alt: Alt | None = None) -> tuple[bool, str]:
+ def get(self, lat: Lat, lon: Lon, alt: Alt | None = None) -> Result[str, str]:
"""Get wind at a specified position (and optionally at altitude)
Implements the GETWIND stack command. The result is reported as
@@ -471,9 +472,6 @@ def get(self, lat: Lat, lon: Lon, alt: Alt | None = None) -> tuple[bool, str]:
- lat, lon: Horizontal position where wind should be determined [deg]
- alt: Altitude at which wind should be determined [m]
(stack input in ft)
-
- Returns:
- tuple: (True, text with wind direction [deg] and speed [kts]).
"""
vn, ve = self.getdata(lat, lon, alt)
@@ -482,4 +480,4 @@ def get(self, lat: Lat, lon: Lon, alt: Alt | None = None) -> tuple[bool, str]:
txt = f"WIND AT {lat:.5f}, {lon:.5f}: {round(wdir):03d}/{round(wspd / kts)}"
- return True, txt
+ return Ok(txt)
diff --git a/tests/integration/test_conflict.py b/tests/integration/test_conflict.py
index 26f319d..ae67e33 100644
--- a/tests/integration/test_conflict.py
+++ b/tests/integration/test_conflict.py
@@ -72,15 +72,17 @@ def test_reso_status_reports_off(self, runtime: MiniSky, run_cmd: RunCommand) ->
output = run_cmd("RESO")
assert "Current CR method: OFF" in output
- def test_rmethh_returns_success_tuple(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
+ def test_rmethh_returns_ok_result(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
run_cmd("RESO MVP")
result = runtime.traffic.cr.setresometh("SPD")
- assert result == (True, "Horizontal resolution method set to SPD")
+ assert result.is_ok()
+ assert result.unwrap() == "Horizontal resolution method set to SPD"
- def test_rmethv_returns_success_tuple(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
+ def test_rmethv_returns_ok_result(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
run_cmd("RESO MVP")
result = runtime.traffic.cr.setresometv("ON")
- assert result == (True, "Vertical resolution method set to ON")
+ assert result.is_ok()
+ assert result.unwrap() == "Vertical resolution method set to ON"
def test_rmethh_via_stack(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
run_cmd("RESO MVP")
@@ -103,9 +105,8 @@ def test_rmethh_requires_mvp(self, runtime: MiniSky, run_cmd: RunCommand) -> Non
def test_resooff_report_mentions_resooff(self, runtime: MiniSky, sim: Simulation) -> None:
result = runtime.traffic.cr.setresooff()
- assert isinstance(result, tuple)
- success, message = result
- assert success
+ assert result.is_ok()
+ message = result.unwrap()
assert "RESOOFF" in message
assert "NORESO" not in message
@@ -123,9 +124,9 @@ def test_zonedh_status_query(self, runtime: MiniSky, run_cmd: RunCommand) -> Non
def test_sethpz_status_uses_default(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
run_cmd("CRE KL204,B744,52,4,45,FL250,350")
- success, message = runtime.traffic.cd.sethpz()
- assert success
- assert f"{runtime.traffic.cd.hpz_def / FT:.2f} ft" in message
+ result = runtime.traffic.cd.sethpz()
+ assert result.is_ok()
+ assert f"{runtime.traffic.cd.hpz_def / FT:.2f} ft" in result.unwrap()
def test_hpz_default_consistent_after_reset(self, runtime: MiniSky, sim: Simulation) -> None:
# reset() must restore the same default as __init__
diff --git a/tests/integration/test_navdata.py b/tests/integration/test_navdata.py
index 9abf648..e378729 100644
--- a/tests/integration/test_navdata.py
+++ b/tests/integration/test_navdata.py
@@ -14,9 +14,9 @@ class TestDefwpt:
def test_defwpt_adds_waypoint(self, runtime: MiniSky, sim: Simulation) -> None:
navdb = runtime.navigation
n = len(navdb.wpid)
- ok, msg = navdb.defwpt("TSTWPT1", 52.0, 4.0, "FIX")
- assert ok
- assert "TSTWPT1" in msg
+ result = navdb.defwpt("TSTWPT1", 52.0, 4.0, "FIX")
+ assert result.is_ok()
+ assert "TSTWPT1" in result.unwrap()
assert len(navdb.wpid) == n + 1
assert len(navdb.wplat) == n + 1
assert len(navdb.wplon) == n + 1
@@ -32,8 +32,8 @@ def test_delwpt_removes_coordinates(self, runtime: MiniSky, sim: Simulation) ->
navdb.defwpt("TSTWPTA", 52.0, 4.0, "FIX")
navdb.defwpt("TSTWPTB", 10.0, 20.0, "FIX")
- ok, _ = navdb.delwpt("TSTWPTA")
- assert ok
+ result = navdb.delwpt("TSTWPTA")
+ assert result.is_ok()
assert "TSTWPTA" not in navdb.wpid
assert len(navdb.wpid) == n + 1
assert len(navdb.wplat) == n + 1
@@ -51,9 +51,9 @@ def test_defwpt_delete_via_lon_delete_keyword(self, runtime: MiniSky, sim: Simul
navdb.defwpt("TSTWPT2", 52.0, 4.0)
# TODO(abraham): there may be an inherited bug in the following line, ignoring for now
- ok, msg = navdb.defwpt("TSTWPT2", 0.0, "delete") # type: ignore
- assert ok
- assert "deleted" in msg
+ result = navdb.defwpt("TSTWPT2", 0.0, "delete") # type: ignore
+ assert result.is_ok()
+ assert "deleted" in result.unwrap()
assert "TSTWPT2" not in navdb.wpid
assert len(navdb.wpid) == n
assert len(navdb.wplat) == n
@@ -62,8 +62,8 @@ def test_defwpt_delete_via_lon_delete_keyword(self, runtime: MiniSky, sim: Simul
def test_defwpt_delete_via_wptype_del(self, runtime: MiniSky, sim: Simulation) -> None:
navdb = runtime.navigation
navdb.defwpt("TSTWPT3", 52.0, 4.0)
- ok, _msg = navdb.defwpt("TSTWPT3", 52.0, 4.0, "DEL")
- assert ok
+ result = navdb.defwpt("TSTWPT3", 52.0, 4.0, "DEL")
+ assert result.is_ok()
assert "TSTWPT3" not in navdb.wpid
def test_delwpt_accepts_lowercase_name(self, runtime: MiniSky, sim: Simulation) -> None:
@@ -71,6 +71,6 @@ def test_delwpt_accepts_lowercase_name(self, runtime: MiniSky, sim: Simulation)
# searched wpid with the raw name, raising ValueError for lowercase input
navdb = runtime.navigation
navdb.defwpt("TSTWPT4", 52.0, 4.0, "FIX")
- ok, _ = navdb.delwpt("tstwpt4")
- assert ok
+ result = navdb.delwpt("tstwpt4")
+ assert result.is_ok()
assert "TSTWPT4" not in navdb.wpid
diff --git a/tests/integration/test_plugin.py b/tests/integration/test_plugin.py
index 0db5566..01a6388 100644
--- a/tests/integration/test_plugin.py
+++ b/tests/integration/test_plugin.py
@@ -12,7 +12,7 @@
import numpy as np
import pytest
-from minisky import MiniSky, MiniSkyConfig
+from minisky import Err, MiniSky, MiniSkyConfig, Ok, Result
from minisky import plugin as plugin_api
from minisky.simulation import Simulation
from minisky.traffic import Traffic
@@ -42,15 +42,15 @@ def load(self) -> object:
runtime.close()
def test_listing(self, runtime: MiniSky) -> None:
- ok, text = runtime.plugins.listing()
- assert ok
- assert "EXAMPLE" in text
+ result = runtime.plugins.listing()
+ assert result.is_ok()
+ assert "EXAMPLE" in result.unwrap()
@pytest.mark.anyio
async def test_unknown_plugin_load_fails(self, runtime: MiniSky) -> None:
- ok, message = await runtime.plugins.load("NOSUCHPLUGIN")
- assert not ok
- assert "not found" in message.lower()
+ result = await runtime.plugins.load("NOSUCHPLUGIN")
+ assert result.is_err()
+ assert "not found" in result.unwrap_err().lower()
def test_discovery_emits_no_deprecation_warning(self, runtime: MiniSky) -> None:
with warnings.catch_warnings():
@@ -83,8 +83,8 @@ def run_command(runtime: MiniSky, command: str) -> str:
async def test_example_commands_and_entity_are_runtime_owned() -> None:
runtime = MiniSky(MiniSkyConfig())
try:
- ok, message = await runtime.plugins.load("EXAMPLE")
- assert ok, message
+ result = await runtime.plugins.load("EXAMPLE")
+ assert result.is_ok(), result.err()
record = runtime.plugins.plugins["EXAMPLE"]
assert record.loaded
assert tuple(runtime.plugins.loaded_plugins) == ("EXAMPLE",)
@@ -94,7 +94,7 @@ async def test_example_commands_and_entity_are_runtime_owned() -> None:
assert "150" in run_command(runtime, "PASSENGERS KL001")
again = await runtime.plugins.load("EXAMPLE")
- assert again == (False, "Plugin EXAMPLE already loaded")
+ assert again == Err("Plugin EXAMPLE already loaded")
finally:
await runtime.aclose()
@@ -105,8 +105,8 @@ async def test_example_entity_sizes_existing_traffic_and_retires() -> None:
runtime = MiniSky(MiniSkyConfig())
runtime.traffic.cre("KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150)
- ok, message = await runtime.plugins.load("EXAMPLE")
- assert ok, message
+ result = await runtime.plugins.load("EXAMPLE")
+ assert result.is_ok(), result.err()
record = runtime.plugins.plugins["EXAMPLE"]
entity = cast(Example, record.entities[0])
assert record.entities == (entity,)
@@ -154,8 +154,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
await entered.wait()
runtime.traffic.cre("KL001", alt=3000.0, spd=150.0)
release.set()
- ok, message = await load_task
- assert ok, message
+ result = await load_task
+ assert result.is_ok(), result.err()
assert entity.names.tolist() == ["KL001"]
finally:
release.set()
@@ -185,8 +185,8 @@ def build(context: plugin_api.PluginContext[Config]) -> plugin_api.PluginSpec:
)
runtime = MiniSky(MiniSkyConfig(plugins={"typed": {"value": 7}}))
try:
- ok, message = await runtime.plugins.load("TYPED")
- assert ok, message
+ result = await runtime.plugins.load("TYPED")
+ assert result.is_ok(), result.err()
state = State(7, runtime.python_random)
assert runtime.variables.varlist["typed"] == (state, ["value", "random"])
assert runtime.plugins.plugins["TYPED"].spec == plugin_api.PluginSpec((state,), state)
@@ -203,10 +203,10 @@ def __init__(self) -> None:
self.values: list[int] = []
@plugin_api.command(arguments="int")
- def record(self, value: int) -> tuple[bool, str]:
+ def record(self, value: int) -> Result[str, str]:
"""Record an integer."""
self.values.append(value)
- return True, f"recorded {value}"
+ return Ok(f"recorded {value}")
component = Component()
@@ -218,8 +218,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
runtime = MiniSky(MiniSkyConfig())
try:
assert "RECORD" not in runtime.commands.cmddict
- ok, message = await runtime.plugins.load("MOUNTED")
- assert ok, message
+ result = await runtime.plugins.load("MOUNTED")
+ assert result.is_ok(), result.err()
command = runtime.commands.cmddict["RECORD"]
assert command.callback.__self__ is component
assert command.brief == "RECORD value"
@@ -253,8 +253,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
install(monkeypatch, FakeEntryPoint("hooks", plugin_api.Plugin(build=build)))
runtime = MiniSky(MiniSkyConfig())
try:
- ok, message = await runtime.plugins.load("HOOKS")
- assert ok, message
+ result = await runtime.plugins.load("HOOKS")
+ assert result.is_ok(), result.err()
runtime.plugins.preupdate()
runtime.plugins.update()
runtime.plugins.preupdate()
@@ -289,8 +289,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
install(monkeypatch, FakeEntryPoint("hooks", plugin_api.Plugin(build=build)))
runtime = MiniSky(MiniSkyConfig())
try:
- ok, message = await runtime.plugins.load("HOOKS")
- assert ok, message
+ result = await runtime.plugins.load("HOOKS")
+ assert result.is_ok(), result.err()
runtime.plugins.update()
runtime.plugins.update()
assert calls == {"broken": 1, "healthy": 2}
@@ -306,19 +306,19 @@ async def test_replacement_visibility_is_runtime_local_and_removed_on_shutdown()
runtime_a = MiniSky(MiniSkyConfig())
runtime_b = MiniSky(MiniSkyConfig())
try:
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT")[0] is False
- assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT")[0] is False
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
+ assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
alt_callback = runtime_a.commands.cmddict["ALT"].callback
- ok, message = await runtime_a.plugins.load("CUSTOMAUTOPILOT")
- assert ok, message
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT")[0] is True
+ result = await runtime_a.plugins.load("CUSTOMAUTOPILOT")
+ assert result.is_ok(), result.err()
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_ok()
assert type(runtime_a.traffic.ap) is CustomAutoPilot
- assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT")[0] is False
+ assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
await runtime_a.plugins.aclose()
assert type(runtime_a.traffic.ap) is Autopilot
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT")[0] is False
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
assert runtime_a.commands.cmddict["ALT"].callback is alt_callback
finally:
await runtime_a.aclose()
@@ -348,10 +348,10 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
runtime = MiniSky(MiniSkyConfig())
try:
runtime.traffic.cre("KL001", alt=3000.0, spd=150.0)
- ok, message = await runtime.plugins.load("ARRAYS")
- assert ok, message
+ result = await runtime.plugins.load("ARRAYS")
+ assert result.is_ok(), result.err()
alt_callback = runtime.commands.cmddict["ALT"].callback
- assert runtime.replaceables.select("AUTOPILOT", "ARRAYAUTOPILOT")[0] is True
+ assert runtime.replaceables.select("AUTOPILOT", "ARRAYAUTOPILOT").is_ok()
selected = cast(ArrayAutopilot, runtime.traffic.ap)
runtime.commands.stack("ALT KL001 FL100")
runtime.simulation.step()
@@ -395,8 +395,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
install(monkeypatch, FakeEntryPoint("lifecycle", plugin_api.Plugin(build=build)))
runtime = MiniSky(MiniSkyConfig())
- ok, message = await runtime.plugins.load("LIFECYCLE")
- assert ok, message
+ result = await runtime.plugins.load("LIFECYCLE")
+ assert result.is_ok(), result.err()
assert events == [("enter", False)]
assert "LIFECYCLE" in runtime.commands.cmddict
assert capability is not None
@@ -435,8 +435,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
install(monkeypatch, FakeEntryPoint("blocked", plugin_api.Plugin(build=build)))
runtime = MiniSky(MiniSkyConfig())
- ok, message = await runtime.plugins.load("BLOCKED")
- assert ok, message
+ result = await runtime.plugins.load("BLOCKED")
+ assert result.is_ok(), result.err()
runtime.commands.stack("BLOCK")
assert not runtime.simulation.step()
await asyncio.sleep(0)
@@ -473,10 +473,10 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
install(monkeypatch, FakeEntryPoint("failedstart", plugin_api.Plugin(build=build)))
runtime = MiniSky(MiniSkyConfig())
- ok, message = await runtime.plugins.load("FAILEDSTART")
+ result = await runtime.plugins.load("FAILEDSTART")
- assert not ok
- assert "startup failed" in message
+ assert result.is_err()
+ assert "startup failed" in result.unwrap_err()
assert "FAILEDSTART" not in runtime.commands.cmddict
assert "failedstart" not in runtime.variables.varlist
assert not runtime.plugins.plugins["FAILEDSTART"].loaded
@@ -542,8 +542,8 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
FakeEntryPoint("second", declaration("second")),
)
runtime = MiniSky(MiniSkyConfig())
- assert (await runtime.plugins.load("FIRST"))[0]
- assert (await runtime.plugins.load("SECOND"))[0]
+ assert (await runtime.plugins.load("FIRST")).is_ok()
+ assert (await runtime.plugins.load("SECOND")).is_ok()
with pytest.raises(ExceptionGroup) as exc_info:
await runtime.aclose()
@@ -564,10 +564,10 @@ async def test_concurrent_duplicate_loads_are_serialized() -> None:
runtime.plugins.load("EXAMPLE"),
runtime.plugins.load("EXAMPLE"),
)
- assert sorted(results) == [
- (False, "Plugin EXAMPLE already loaded"),
- (True, "Successfully loaded plugin EXAMPLE"),
- ]
+ assert set(results) == {
+ Err("Plugin EXAMPLE already loaded"),
+ Ok("Successfully loaded plugin EXAMPLE"),
+ }
finally:
await runtime.aclose()
diff --git a/tests/integration/test_route_autopilot.py b/tests/integration/test_route_autopilot.py
index d159cc6..188c5d8 100644
--- a/tests/integration/test_route_autopilot.py
+++ b/tests/integration/test_route_autopilot.py
@@ -128,7 +128,7 @@ def test_addwpt_accepts_string_callsign(
) -> None:
# addwpt() with a callsign string used to crash on the callsign lookup
result = route_commands.addwpt(runtime.traffic, aircraft, "52.5,5.0")
- assert result is True
+ assert result.is_ok()
route = runtime.traffic.ap.route[0]
assert route.wplat[0] == pytest.approx(52.5)
assert route.wplon[0] == pytest.approx(5.0)
@@ -180,7 +180,7 @@ def test_at_wpt_sets_alt_and_spd_constraints(
run_cmd(f"ADDWPT {aircraft} 53.0,6.0")
route = runtime.traffic.ap.route[0]
result = route_commands.at_wpt(runtime.traffic, 0, route.wpname[1], "FL090/250")
- assert result is True
+ assert result.is_ok()
assert route.wpalt[1] == pytest.approx(9000 * FT, rel=1e-3)
assert route.wpspd[1] == pytest.approx(250 * KTS, rel=1e-3)
diff --git a/tests/integration/test_stack.py b/tests/integration/test_stack.py
index 4c95658..4f2f9f2 100644
--- a/tests/integration/test_stack.py
+++ b/tests/integration/test_stack.py
@@ -158,10 +158,10 @@ def test_help_writes_command_reference(
# HELP >filename writes the reference to ./docs/
monkeypatch.chdir(tmp_path)
(tmp_path / "docs").mkdir()
- success, msg = runtime.commands.showhelp(">ref.txt")
- assert success
+ result = runtime.commands.showhelp(">ref.txt")
+ assert result.is_ok(), result.err()
ref = tmp_path / "docs" / "ref.txt"
- assert ref.exists(), msg
+ assert ref.exists(), result.ok()
content = ref.read_text()
assert content.startswith("Command\tDescription\tUsage")
assert "\nCRE\t" in content
diff --git a/tests/integration/test_streaming.py b/tests/integration/test_streaming.py
index 705d69b..363ce9b 100644
--- a/tests/integration/test_streaming.py
+++ b/tests/integration/test_streaming.py
@@ -75,9 +75,9 @@ def test_dtmult_sets_runner_speed(runtime: MiniSky, sim: Simulation, run_cmd: Ru
def test_dtmult_rejects_non_positive(runtime: MiniSky, sim: Simulation) -> None:
- ok, msg = runtime.runner.setspeed(0)
- assert ok is False
- assert "positive" in msg.lower()
+ result = runtime.runner.setspeed(0)
+ assert result.is_err()
+ assert "positive" in result.unwrap_err().lower()
def test_hub_skips_publish_without_subscribers(runtime: MiniSky) -> None:
diff --git a/tests/integration/test_tangram_bridge.py b/tests/integration/test_tangram_bridge.py
index 4370e31..ab9c5c3 100644
--- a/tests/integration/test_tangram_bridge.py
+++ b/tests/integration/test_tangram_bridge.py
@@ -42,8 +42,8 @@ def bridge(
plugin_runtime = runtime.plugins._plugin_runtime()
plugin_runtime._activate()
plugin_runtime.subscribe_console(bridge.capture_console)
- ok, msg = bridge.start(plugin_runtime)
- assert ok, msg
+ result = bridge.start(plugin_runtime)
+ assert result.is_ok(), result.err()
# The I/O thread subscribes asynchronously; commands published before the
# subscription is live would be silently lost (pub/sub has no replay).
assert bridge.ready.wait(timeout=5.0), "bridge did not subscribe in time"
diff --git a/tests/integration/test_traffic.py b/tests/integration/test_traffic.py
index 68a464e..d32e488 100644
--- a/tests/integration/test_traffic.py
+++ b/tests/integration/test_traffic.py
@@ -15,10 +15,8 @@
class TestCreate:
def test_cre_single(self, runtime: MiniSky, sim: Simulation) -> None:
- ok, _msg = runtime.traffic.cre(
- "KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150
- )
- assert ok
+ result = runtime.traffic.cre("KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150)
+ assert result.is_ok()
assert runtime.traffic.ntraf == 1
assert runtime.traffic.callsign[0] == "KL001"
assert runtime.traffic.lat[0] == pytest.approx(52.0)
@@ -31,13 +29,13 @@ def test_cre_lowercase_callsign_is_uppercased(self, runtime: MiniSky, sim: Simul
def test_cre_duplicate_callsign_rejected(self, runtime: MiniSky, sim: Simulation) -> None:
runtime.traffic.cre("KL001")
- ok, _msg = runtime.traffic.cre("KL001")
- assert not ok
+ result = runtime.traffic.cre("KL001")
+ assert result.is_err()
assert runtime.traffic.ntraf == 1
def test_mcre_multiple(self, runtime: MiniSky, sim: Simulation) -> None:
- ok, _ = runtime.traffic.mcre(5)
- assert ok
+ result = runtime.traffic.mcre(5)
+ assert result.is_ok()
assert runtime.traffic.ntraf == 5
assert len(set(runtime.traffic.callsign)) == 5
@@ -178,7 +176,8 @@ def test_renameac_updates_pending_conditions(self, runtime: MiniSky, sim: Simula
class TestWind:
def test_wind_add_get_roundtrip(self, runtime: MiniSky, sim: Simulation) -> None:
wind = runtime.traffic.wind
- assert wind.add(52.0, 4.0, 270.0, 20.0) is True # from 270 deg, 20 kts
+ result = wind.add(52.0, 4.0, 270.0, 20.0) # from 270 deg, 20 kts
+ assert result.is_ok()
vn, ve = wind.getdata(52.0, 4.0, 0.0)
assert ve == pytest.approx(20 * KTS) # westerly wind blows eastward
assert vn == pytest.approx(0.0, abs=1e-9)
@@ -197,7 +196,8 @@ def test_wind_del_clears_field(self, runtime: MiniSky, sim: Simulation) -> None:
wind.add(52.0, 4.0, 270.0, 20.0)
assert wind.winddim > 0
# TODO(abraham): possible bug!
- assert wind.add(52.0, 4.0, "DEL") is True # type: ignore
+ result = wind.add(52.0, 4.0, "DEL") # type: ignore
+ assert result.is_ok()
assert wind.winddim == 0
assert len(wind.lat) == 0
@@ -208,7 +208,8 @@ def test_wind_del_not_shadowed_by_altitude_form(
wind.add(52.0, 4.0, 270.0, 20.0)
# With 3+ winddata elements DEL used to fall into the alt/dir/spd branch
# TODO(abraham): possible bug!
- assert wind.add(52.0, 4.0, "DEL", None, None) is True # type: ignore
+ result = wind.add(52.0, 4.0, "DEL", None, None) # type: ignore
+ assert result.is_ok()
assert wind.winddim == 0
def test_wind_via_stack_two_element_form(self, runtime: MiniSky, run_cmd: RunCommand) -> None:
diff --git a/tests/unit/test_areafilter.py b/tests/unit/test_areafilter.py
index d6ba8d3..332d9d5 100644
--- a/tests/unit/test_areafilter.py
+++ b/tests/unit/test_areafilter.py
@@ -22,8 +22,8 @@ def check_single(
class TestDefineArea:
def test_define_box_and_has_area(self, area_filter: AreaFilter) -> None:
- ok, _msg = area_filter.define_area("BOX1", "BOX", [52.0, 4.0, 53.0, 5.0])
- assert ok
+ result = area_filter.define_area("BOX1", "BOX", [52.0, 4.0, 53.0, 5.0])
+ assert result.is_ok()
assert area_filter.has_area("BOX1")
def test_unknown_area_absent(self, area_filter: AreaFilter) -> None:
From 07a42edec1d597a1f8e09847d3222546d9eb9da3 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 11:48:08 +0800
Subject: [PATCH 5/8] refactor(cli): remove duplicate `server.main()` and
remove env vars
- server should be launched with `minisky.server`
- allow controlling server host/port in config.toml
---
AGENTS.md | 2 +-
docs/guides/cli.md | 2 +-
packages/minisky/minisky/cli.py | 33 ++++++++++++-------------
packages/minisky/minisky/core/config.py | 20 ++++++++++-----
packages/minisky/minisky/server.py | 17 -------------
5 files changed, 32 insertions(+), 42 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index aa10701..9db2c85 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -23,7 +23,7 @@ minisky console # interactive console
```
The FastAPI app lives in `packages/minisky/minisky/server.py`; `minisky server` is the CLI entry point
-(`MINISKY_HOST`/`MINISKY_PORT` env vars, default `0.0.0.0:8000`).
+(default `0.0.0.0:8000`; configure `[server]` in TOML or override with `--host` and `--port`).
## Architecture
diff --git a/docs/guides/cli.md b/docs/guides/cli.md
index cfa514f..91a5a8a 100644
--- a/docs/guides/cli.md
+++ b/docs/guides/cli.md
@@ -12,7 +12,7 @@ uv run minisky --help
| Command | Purpose |
| --- | --- |
| `minisky run --scenario FILE [--speed N] [--config FILE]` | Run a scenario file without interaction. |
-| `minisky server [--host HOST] [--port PORT] [--reload] [--config FILE]` | Start the REST and WebSocket API server. |
+| `minisky server [--host HOST] [--port PORT] [--reload] [--config FILE]` | Start the REST and WebSocket API server; CLI bind options override `[server]` config. |
| `minisky console [--server URL] [--port PORT]` | Open an interactive console against a running server. |
| `minisky stream [--url URL] [--raw]` | Print snapshots from the `/stream` WebSocket. |
diff --git a/packages/minisky/minisky/cli.py b/packages/minisky/minisky/cli.py
index 5da21b9..402304f 100644
--- a/packages/minisky/minisky/cli.py
+++ b/packages/minisky/minisky/cli.py
@@ -19,7 +19,7 @@
from prompt_toolkit.history import FileHistory
from pydantic import ValidationError
-from minisky.core.config import MiniSkyConfig
+from minisky.core.config import MiniSkyConfig, default_user_config_toml_path
if TYPE_CHECKING:
from minisky.runtime import MiniSky
@@ -36,14 +36,13 @@
completer = NestedCompleter.from_nested_dict({"load": path_completer, "/load": path_completer})
-def _load_config(path: Path | None) -> MiniSkyConfig | None:
- if path is None:
- return None
-
- selected = path.expanduser()
+def _load_config(path: Path | None) -> MiniSkyConfig:
+ selected = path.expanduser() if path is not None else default_user_config_toml_path()
try:
return MiniSkyConfig.from_path(selected)
except FileNotFoundError as exc:
+ if path is None:
+ return MiniSkyConfig()
raise typer.BadParameter(
f"config file not found: {selected}",
param_hint="--config",
@@ -55,16 +54,16 @@ def _load_config(path: Path | None) -> MiniSkyConfig | None:
) from exc
-def _new_runtime(config_path: Path | None, scenario: str | None = None) -> MiniSky:
- """Construct a runtime from explicit or default configuration."""
+def _new_runtime(config: MiniSkyConfig, scenario: str | None = None) -> MiniSky:
+ """Construct a runtime from validated configuration."""
from minisky import MiniSky
- return MiniSky(config=_load_config(config_path), scenario=scenario)
+ return MiniSky(config=config, scenario=scenario)
async def _run_scenario(scenario: str, speed: int, config_path: Path | None) -> None:
"""Initialise the simulator with a scenario and run it to completion."""
- async with _new_runtime(config_path, scenario) as runtime:
+ async with _new_runtime(_load_config(config_path), scenario) as runtime:
await runtime.plugins.load_configured()
runtime.runner.speed = speed
await runtime.run()
@@ -82,12 +81,8 @@ def run_cmd(
@app.command("server")
def server_cmd(
- host: Annotated[str, typer.Option(help="Host address to bind.")] = os.environ.get(
- "MINISKY_HOST", "0.0.0.0"
- ),
- port: Annotated[int, typer.Option(help="TCP port to bind.")] = int(
- os.environ.get("MINISKY_PORT", "8000")
- ),
+ host: Annotated[str | None, typer.Option(help="Host address to bind.")] = None,
+ port: Annotated[int | None, typer.Option(help="TCP port to bind.")] = None,
reload: Annotated[bool, typer.Option(help="Enable uvicorn auto-reload.")] = False,
config: _ConfigOption = None,
) -> None:
@@ -101,6 +96,10 @@ def server_cmd(
param_hint="--config",
)
+ loaded_config = _load_config(config)
+ host = host if host is not None else loaded_config.server.host
+ port = port if port is not None else loaded_config.server.port
+
if reload:
uvicorn.run(
"minisky.server:create_app",
@@ -114,7 +113,7 @@ def server_cmd(
from minisky.server import create_app
uvicorn.run(
- create_app(_new_runtime(config)),
+ create_app(_new_runtime(loaded_config)),
host=host,
port=port,
)
diff --git a/packages/minisky/minisky/core/config.py b/packages/minisky/minisky/core/config.py
index d59728d..9f12c48 100644
--- a/packages/minisky/minisky/core/config.py
+++ b/packages/minisky/minisky/core/config.py
@@ -7,7 +7,7 @@
from pathlib import Path
from typing import Annotated, Any, TypeAlias
-import annotated_types
+from annotated_types import Ge, Gt, Le
from pydantic import BaseModel, ConfigDict, Field
from pydantic.functional_validators import BeforeValidator
@@ -16,16 +16,24 @@
PluginId: TypeAlias = Annotated[str, BeforeValidator(validate_plugin_id)]
+class ServerConfig(BaseModel):
+ model_config = ConfigDict(frozen=True, extra="forbid")
+
+ host: str = "0.0.0.0"
+ port: Annotated[int, Ge(0), Le(65535)] = 8000
+
+
class MiniSkyConfig(BaseModel):
"""Validated configuration for a MiniSky runtime."""
model_config = ConfigDict(frozen=True, extra="forbid")
- asas_dtlookahead: Annotated[float, Field(), annotated_types.Ge(0)] = 300.0
- asas_pzr: Annotated[float, Field(), annotated_types.Gt(0)] = 5.0
- asas_pzh: Annotated[float, Field(), annotated_types.Gt(0)] = 1000.0
- asas_marh: Annotated[float, Field(), annotated_types.Gt(0)] = 1.05
- asas_marv: Annotated[float, Field(), annotated_types.Gt(0)] = 1.05
+ asas_dtlookahead: Annotated[float, Ge(0)] = 300.0
+ asas_pzr: Annotated[float, Gt(0)] = 5.0
+ asas_pzh: Annotated[float, Gt(0)] = 1000.0
+ asas_marh: Annotated[float, Gt(0)] = 1.05
+ asas_marv: Annotated[float, Gt(0)] = 1.05
+ server: ServerConfig = Field(default_factory=ServerConfig)
plugins: dict[PluginId, dict[str, Any]] = Field(default_factory=dict)
@classmethod
diff --git a/packages/minisky/minisky/server.py b/packages/minisky/minisky/server.py
index 6b02dab..2099841 100644
--- a/packages/minisky/minisky/server.py
+++ b/packages/minisky/minisky/server.py
@@ -319,20 +319,3 @@ def create_router() -> APIRouter:
router.add_api_route("/plugins", list_plugins, methods=["GET"])
router.add_api_route("/plugins/load/{name}", load_plugin, methods=["GET"])
return router
-
-
-def main() -> None:
- """Console-script entry point: serve the API with uvicorn.
-
- Host and port are read from `MINISKY_HOST` (default `0.0.0.0`) and
- `MINISKY_PORT` (default `8000`).
- """
- import uvicorn
-
- host = os.environ.get("MINISKY_HOST", "0.0.0.0")
- port = int(os.environ.get("MINISKY_PORT", "8000"))
- uvicorn.run("minisky.server:create_app", factory=True, host=host, port=port)
-
-
-if __name__ == "__main__":
- main()
From 00727934cf907e66a48f38d7996e38ae6ad7bb37 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 14:19:35 +0800
Subject: [PATCH 6/8] refactor: replace most bare tuples with NamedTuples
- parser is lighly handled because of #37
- changes to the CAS/Mach/TAS representation is deferred to #40
---
packages/minisky/minisky/stack/__init__.py | 34 +++++---
packages/minisky/minisky/stack/argparser.py | 40 ++++++---
packages/minisky/minisky/tools/aero.py | 74 +++++++++++------
packages/minisky/minisky/tools/convert.py | 33 +++++---
.../minisky/minisky/traffic/activewpdata.py | 16 ++--
.../minisky/minisky/traffic/asas/detection.py | 83 ++++++++++++-------
packages/minisky/minisky/traffic/asas/mvp.py | 56 +++++++------
.../minisky/traffic/asas/resolution.py | 29 +++++--
.../minisky/traffic/performance/perfoap.py | 57 ++++++++-----
.../minisky/traffic/performance/thrust.py | 18 ++--
packages/minisky/minisky/traffic/route.py | 57 +++++++++----
11 files changed, 322 insertions(+), 175 deletions(-)
diff --git a/packages/minisky/minisky/stack/__init__.py b/packages/minisky/minisky/stack/__init__.py
index b30500d..5728e27 100644
--- a/packages/minisky/minisky/stack/__init__.py
+++ b/packages/minisky/minisky/stack/__init__.py
@@ -34,7 +34,7 @@
from io import StringIO
from pathlib import Path
from threading import Lock
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
@@ -233,12 +233,20 @@ def brieftext(self) -> str:
"""Return the brief usage text."""
return self.brief
- def _get_arguments(self, arguments) -> tuple:
- """Get arguments from string, or tuple/list."""
+ class ArgumentSpec(NamedTuple):
+ annotation: str
+ """Parser annotation."""
+ optional: bool
+ """Whether the argument may be omitted."""
+
+ def _get_arguments(
+ self, arguments: str | list[ArgumentSpec] | tuple[ArgumentSpec, ...]
+ ) -> tuple[ArgumentSpec, ...]:
+ """Get arguments from string, or typed argument specifications."""
if isinstance(arguments, (tuple, list)):
return tuple(arguments)
# Assume it is a comma-separated string
- argtypes = []
+ argtypes: list[Command.ArgumentSpec] = []
# Process and reduce annotation string from left to right
# First cut at square brackets, then take separate argument types
@@ -253,8 +261,7 @@ def _get_arguments(self, arguments) -> tuple:
)
types = [t.strip() for t in arguments[:cut].strip("[,] ").split(",")]
- # Returned argtypes are tuples of type and optional status
- argtypes += [(t, opt or t == "...") for t in types if t]
+ argtypes.extend(self.ArgumentSpec(t, opt or t == "...") for t in types if t)
arguments = arguments[cut:].lstrip(",]")
return tuple(argtypes)
@@ -850,14 +857,15 @@ def get_scenname(self) -> str:
or otherwise the filename of the scenario."""
return self.scenname
- def get_scendata(self) -> tuple:
- """Return the scenario data that was loaded from a scenario file.
+ class ScenarioData(NamedTuple):
+ times: list[float]
+ """Buffered command execution times [s]."""
+ commands: list[str]
+ """Buffered scenario command lines."""
- Returns:
- tuple: (scentime, scencmd), the lists of command times [s] and
- command lines still buffered for execution.
- """
- return self.scentime, self.scencmd
+ def get_scendata(self) -> ScenarioData:
+ """Return the scenario data that was loaded from a scenario file."""
+ return self.ScenarioData(self.scentime, self.scencmd)
def set_scendata(self, newtime, newcmd) -> None:
"""Set the scenario data. This is used by the batch logic."""
diff --git a/packages/minisky/minisky/stack/argparser.py b/packages/minisky/minisky/stack/argparser.py
index c0fa071..8da2f69 100644
--- a/packages/minisky/minisky/stack/argparser.py
+++ b/packages/minisky/minisky/stack/argparser.py
@@ -21,7 +21,7 @@
import re
from collections.abc import Callable
from types import SimpleNamespace, UnionType
-from typing import TYPE_CHECKING, Annotated, Any, Union, get_args, get_origin
+from typing import TYPE_CHECKING, Annotated, Any, Protocol, TypeAlias, Union, get_args, get_origin
from minisky.result import Err, Ok
from minisky.tools.convert import (
@@ -41,6 +41,22 @@
from minisky.tools.navdata import Navdatabase
from minisky.traffic import Traffic
+
+# TODO(abraham): while we would like to migrate all bare tuple[X, Y, Z] to
+# namedtuples/proper dataclasses, right now each Parser subclass returns a
+# different type.
+# we would like to eventually remove implementation inheritance altogether when
+# we work on issue #37, so we just keep types simple for now.
+
+ParserResult: TypeAlias = tuple[Any, ...]
+
+
+class ParserProtocol(Protocol):
+ size: int
+
+ def parse(self, argstring: str) -> ParserResult: ...
+
+
# Regular expression for argument parser
# Reading the regular expression:
# [\'"]? : skip potential opening quote
@@ -54,10 +70,10 @@ def _match_groups(argstring: str) -> tuple[str, str]:
"""Match argstring against re_getarg (which always matches) and return groups."""
m = re_getarg.match(argstring)
assert m is not None
- return m.groups() # type: ignore[return-value]
+ return m.group(1), m.group(2)
-def getnextarg(cmdstring: str) -> tuple:
+def getnextarg(cmdstring: str) -> tuple[str, str]:
"""Return first argument and remainder of command string from cmdstring.
Arguments are separated by whitespace and/or a comma; quoted arguments
@@ -95,7 +111,7 @@ class Parameter:
def __init__(
self,
param: inspect.Parameter,
- parsers: dict[str, Parser | None],
+ parsers: dict[str, ParserProtocol | None],
annotation: str = "",
isopt: bool | None = None,
) -> None:
@@ -216,10 +232,10 @@ class Parser:
# Output size of this parser
size = 1
- def __init__(self, parsefun: Callable[..., Any] | None = None) -> None:
+ def __init__(self, parsefun: Callable[[str], Any] | None = None) -> None:
self.parsefun = parsefun
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> ParserResult:
"""Parse the next argument from argstring.
Args:
@@ -236,7 +252,7 @@ def parse(self, argstring: str) -> tuple:
class StringArg(Parser):
"""Argument parser that simply consumes the entire remaining text string."""
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> tuple[str, str]:
"""Return the complete remaining text as a single string argument."""
return argstring, ""
@@ -248,7 +264,7 @@ def __init__(self, argument_parser: ArgumentParser) -> None:
super().__init__()
self.argument_parser = argument_parser
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> tuple[Any, str]:
"""Parse a callsign or group name into traffic index/indices.
For an aircraft callsign the traffic index is returned and the
@@ -295,7 +311,7 @@ def __init__(self, argument_parser: ArgumentParser) -> None:
super().__init__()
self.argument_parser = argument_parser
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> tuple[str, str]:
"""Combine one or two arguments into a single waypoint position text.
Aircraft ids are translated to a "lat,lon" text; lat/lon pairs and
@@ -343,7 +359,7 @@ def __init__(self, argument_parser: ArgumentParser) -> None:
super().__init__()
self.argument_parser = argument_parser
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> tuple[float, float, str]:
"""Parse one or two arguments into a lat/lon position.
Also updates the parser reference position to the parsed location.
@@ -401,7 +417,7 @@ def parse(self, argstring: str) -> tuple:
class PandirArg(Parser):
"""Parse pan direction commands."""
- def parse(self, argstring: str) -> tuple:
+ def parse(self, argstring: str) -> tuple[str, str]:
"""Parse a screen pan direction (LEFT, RIGHT, UP/ABOVE, or DOWN).
Raises:
@@ -438,7 +454,7 @@ def __init__(self, traffic: Traffic, navigation: Navdatabase, console: ConsoleIO
# Stack reference data namespace
self.refdata = SimpleNamespace(lat=None, lon=None, alt=None, acidx=-1, hdg=None, cas=None)
- self.parsers: dict[str, Parser | None] = {
+ self.parsers: dict[str, ParserProtocol | None] = {
"*": None,
"txt": Parser(str.upper),
"word": Parser(str),
diff --git a/packages/minisky/minisky/tools/aero.py b/packages/minisky/minisky/tools/aero.py
index 584baae..e96decc 100644
--- a/packages/minisky/minisky/tools/aero.py
+++ b/packages/minisky/minisky/tools/aero.py
@@ -19,6 +19,8 @@
CAS/Mach threshold supplied by the owning runtime.
"""
+from typing import NamedTuple
+
import numpy as np
# International standard atmpshere only up to 72000 ft / 22 km
@@ -78,16 +80,20 @@
# ------------------------------------------------------------------------------
# Vectorized aero functions
# ------------------------------------------------------------------------------
-def vatmos(h: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+class VectorAtmosphere(NamedTuple):
+ pressure: np.ndarray
+ """Atmospheric pressure [Pa]."""
+ density: np.ndarray
+ """Air density [kg/m³]."""
+ temperature: np.ndarray
+ """Air temperature [K]."""
+
+
+def vatmos(h: np.ndarray) -> VectorAtmosphere:
"""Calculate atmospheric pressure, density, and temperature for a given altitude.
Arguments:
- h: Altitude [m]
-
- Returns:
- - p: Pressure [Pa]
- - rho: Density [kg / m3]
- - T: Temperature [K]
"""
# Temp
T = vtemp(h)
@@ -100,7 +106,7 @@ def vatmos(h: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
# Pressure
p = rho * R * T
- return p, rho, T
+ return VectorAtmosphere(p, rho, T)
def vtemp(h: np.ndarray) -> np.ndarray:
@@ -287,11 +293,20 @@ def vcas2mach(cas: np.ndarray, h: np.ndarray) -> np.ndarray:
return M
+class VectorAirspeeds(NamedTuple):
+ true: np.ndarray
+ """True airspeed [m/s]."""
+ calibrated: np.ndarray
+ """Calibrated airspeed [m/s]."""
+ mach: np.ndarray
+ """Mach number [-]."""
+
+
def vcasormach(
spd: np.ndarray,
h: np.ndarray,
threshold: float,
-) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+) -> VectorAirspeeds:
"""Interpret input speed as either CAS or a Mach number, and return TAS, CAS, and Mach.
Arguments:
@@ -299,17 +314,12 @@ def vcasormach(
CAS/Mach threshold. Otherwise interpreted as CAS [m/s].
- h: Altitude [m]
- threshold: Upper bound below which positive speed values are Mach numbers.
-
- Returns:
- - tas: True airspeed [m/s]
- - cas: Calibrated airspeed [m/s]
- - mach: Mach number [-]
"""
ismach = np.logical_and(spd > 0.1, spd < threshold)
tas = np.where(ismach, vmach2tas(spd, h), vcas2tas(spd, h))
cas = np.where(ismach, vtas2cas(tas, h), spd)
mach = np.where(ismach, spd, vtas2mach(tas, h))
- return tas, cas, mach
+ return VectorAirspeeds(tas, cas, mach)
def vcasormach2tas(
@@ -359,7 +369,16 @@ def crossoveralt(cas: float, mach: float) -> float:
# ------------------------------------------------------------------------------
# Scalar aero functions
# ------------------------------------------------------------------------------
-def atmos(h: float) -> tuple[float, float, float]:
+class ScalarAtmosphere(NamedTuple):
+ pressure: float
+ """Atmospheric pressure [Pa]."""
+ density: float
+ """Air density [kg/m³]."""
+ temperature: float
+ """Air temperature [K]."""
+
+
+def atmos(h: float) -> ScalarAtmosphere:
"""International Standard Atmosphere calculator (scalar version).
Uses the full multi-layer ISA table up to the mesosphere, with base
@@ -370,10 +389,6 @@ def atmos(h: float) -> tuple[float, float, float]:
Args:
h: Altitude [m], 0.0 < h < 84852.0 (clipped when outside range,
integer input allowed).
-
- Returns:
- tuple: (p, rho, T): pressure [Pa], density [kg/m3], and
- temperature [K].
"""
# Constants
@@ -434,7 +449,7 @@ def atmos(h: float) -> tuple[float, float, float]:
p = p0[i] * ((T / T0[i]) ** (-g0 / (a[i] * R)))
rho = p / (R * T)
- return p, rho, T
+ return ScalarAtmosphere(p, rho, T)
def temp(h: float) -> float:
@@ -671,11 +686,22 @@ def cas2mach(cas: float, h: float) -> float:
return M
+class ScalarAirspeeds(NamedTuple):
+ true: float
+ """True airspeed [m/s]."""
+ calibrated: float
+ """Calibrated airspeed [m/s]."""
+ mach: float
+ """Mach number [-]."""
+
+
+# TODO(abraham): eventually we want to get rid of the concept of "threshold-encoded" floats
+# not typing it properly for now. see issue #40
def casormach(
spd: float,
h: float,
threshold: float,
-) -> tuple[float, float, float]:
+) -> ScalarAirspeeds:
"""Interpret input speed as either CAS or a Mach number (scalar version).
The speed is treated as a Mach number when 0.1 < spd < threshold
@@ -685,10 +711,6 @@ def casormach(
spd: Airspeed: Mach number [-] or calibrated airspeed [m/s].
h: Altitude [m].
threshold: Upper bound below which positive speed values are Mach numbers.
-
- Returns:
- tuple: (tas, cas, m): true airspeed [m/s], calibrated airspeed
- [m/s], and Mach number [-].
"""
if 0.1 < spd < threshold:
# Interpret spd as Mach number
@@ -700,7 +722,7 @@ def casormach(
tas = cas2tas(spd, h)
cas = spd
m = cas2mach(spd, h)
- return tas, cas, m
+ return ScalarAirspeeds(tas, cas, m)
def casormach2tas(
diff --git a/packages/minisky/minisky/tools/convert.py b/packages/minisky/minisky/tools/convert.py
index 4b761b1..124135b 100644
--- a/packages/minisky/minisky/tools/convert.py
+++ b/packages/minisky/minisky/tools/convert.py
@@ -9,6 +9,7 @@
"""
from time import gmtime, strftime
+from typing import NamedTuple
import numpy as np
@@ -220,15 +221,21 @@ def txt2tas(txt: str, h: float) -> float:
return acspd
-def col2rgb(txt: str) -> tuple[int, int, int]:
+class RGB(NamedTuple):
+ red: int
+ """Red channel [0-255]."""
+ green: int
+ """Green channel [0-255]."""
+ blue: int
+ """Blue channel [0-255]."""
+
+
+def col2rgb(txt: str) -> RGB:
"""Convert named color to R,G,B values (integer per component, 0-255).
Args:
txt: Colour name (e.g. "red", "amber"); unknown names default
to white.
-
- Returns:
- tuple: (R, G, B) integer components in the range 0-255.
"""
cols = {
"black": (0, 0, 0),
@@ -246,7 +253,7 @@ def col2rgb(txt: str) -> tuple[int, int, int]:
except KeyError:
rgb = cols["white"] # default
- return rgb
+ return RGB(*rgb)
def degto180(angle: float | np.ndarray) -> float | np.ndarray:
@@ -385,16 +392,22 @@ def latlon2txt(lat: float, lon: float) -> str:
return lat2txt(lat) + " " + lon2txt(lon)
-def float2degminsec(x: float) -> tuple[int, float, float]:
+class DegreesMinutesSeconds(NamedTuple):
+ degrees: int
+ """Whole degrees [deg]."""
+ minutes: float
+ """Whole arcminutes [arcmin]."""
+ seconds: float
+ """Whole arcseconds [arcsec]."""
+
+
+def float2degminsec(x: float) -> DegreesMinutesSeconds:
"""Split a positive angle in degrees into whole degrees, minutes, and seconds.
Args:
x: Angle [deg] (positive).
-
- Returns:
- tuple: (degrees, minutes, seconds) of the angle.
"""
deg = int(x)
minutes = int(x * 60.0) - deg * 60.0
sec = int(x * 3600.0) - deg * 3600.0 - minutes * 60.0
- return deg, minutes, sec
+ return DegreesMinutesSeconds(deg, minutes, sec)
diff --git a/packages/minisky/minisky/traffic/activewpdata.py b/packages/minisky/minisky/traffic/activewpdata.py
index eb79cd0..1d26b1a 100644
--- a/packages/minisky/minisky/traffic/activewpdata.py
+++ b/packages/minisky/minisky/traffic/activewpdata.py
@@ -10,7 +10,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
@@ -248,6 +248,13 @@ def reached(
# Return indices for which condition is True/1.0 for a/c where we have reached waypoint
return swreached
+ # TODO(abraham): maybe make this Generic over float/np/any array?
+ class TurnGeometry(NamedTuple):
+ distance: Any
+ """Turn-initiation distance [m]."""
+ radius: Any
+ """Turn radius [m]."""
+
# Calculate turn distance for array or scalar
def calcturn(
self,
@@ -258,7 +265,7 @@ def calcturn(
turnrad: Any = -999.0,
turnhdgr: Any = -999.0,
flyturn: Any = False,
- ) -> tuple:
+ ) -> TurnGeometry:
"""Calculate the turn-initiation distance and turn radius.
Works on scalars as well as numpy arrays. The turn radius follows,
@@ -277,9 +284,6 @@ def calcturn(
turnhdgr: Specified turn heading rate [deg/s]
(<0 = not specified).
flyturn: Fly-turn switch (use the specified turn parameters).
-
- Returns:
- tuple: (turn distance [m], turn radius [m]).
"""
# Tas is also used ti
@@ -304,4 +308,4 @@ def calcturn(
turndist = np.abs(
turnrad * np.tan(np.radians(0.5 * np.abs(degto180(wpqdr % 360.0 - next_wpqdr % 360.0))))
)
- return turndist, turnrad
+ return self.TurnGeometry(turndist, turnrad)
diff --git a/packages/minisky/minisky/traffic/asas/detection.py b/packages/minisky/minisky/traffic/asas/detection.py
index d39af2b..fc5a70a 100644
--- a/packages/minisky/minisky/traffic/asas/detection.py
+++ b/packages/minisky/minisky/traffic/asas/detection.py
@@ -20,7 +20,7 @@
from __future__ import annotations
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
from scipy.spatial import KDTree
@@ -38,10 +38,34 @@
RE = 6371000.0
-def _noconflicts(ntraf: int) -> tuple:
+# TODO(abraham): model callsign pairs as a named ConflictPair record.
+class ConflictDetectionResult(NamedTuple):
+ confpairs: list[tuple[str, str]]
+ """Conflicting callsign pairs, in both directions."""
+ lospairs: list[tuple[str, str]]
+ """Callsign pairs in loss of separation."""
+ inconf: np.ndarray
+ """Per-aircraft in-conflict flags [-]."""
+ tcpamax: np.ndarray
+ """Per-aircraft maximum time to closest point of approach [s]."""
+ qdr: np.ndarray
+ """Bearing from ownship to intruder per conflict [deg]."""
+ dist: np.ndarray
+ """Current horizontal distance per conflict [m]."""
+ dcpa: np.ndarray
+ """Horizontal distance at closest point of approach per conflict [m]."""
+ tcpa: np.ndarray
+ """Time to closest point of approach per conflict [s]."""
+ tLOS: np.ndarray
+ """Time until loss of separation per conflict [s]."""
+ dalt: np.ndarray
+ """Current altitude difference per conflict [m]."""
+
+
+def _noconflicts(ntraf: int) -> ConflictDetectionResult:
"""Detection result for a timestep without any conflicts or LoS."""
empty = np.array([])
- return (
+ return ConflictDetectionResult(
[],
[],
np.zeros(ntraf, dtype=bool),
@@ -358,18 +382,18 @@ def update(self, ownship: Any, intruder: Any) -> None:
if not self.activate:
return
- (
- self.confpairs,
- self.lospairs,
- self.inconf,
- self.tcpamax,
- self.qdr,
- self.dist,
- self.dcpa,
- self.tcpa,
- self.tLOS,
- self.dalt,
- ) = self.detect(ownship, intruder, self.rpz, self.hpz, self.dtlookahead)
+ result = self.detect(ownship, intruder, self.rpz, self.hpz, self.dtlookahead)
+ # TODO(abraham): consider storing the entire result
+ self.confpairs = result.confpairs
+ self.lospairs = result.lospairs
+ self.inconf = result.inconf
+ self.tcpamax = result.tcpamax
+ self.qdr = result.qdr
+ self.dist = result.dist
+ self.dcpa = result.dcpa
+ self.tcpa = result.tcpa
+ self.tLOS = result.tLOS
+ self.dalt = result.dalt
# confpairs has conflicts observed from both sides (a, b) and (b, a)
# confpairs_unique keeps only one of these
@@ -383,6 +407,12 @@ def update(self, ownship: Any, intruder: Any) -> None:
self.confpairs_unique = confpairs_unique
self.lospairs_unique = lospairs_unique
+ class VerticalInterval(NamedTuple):
+ entry: np.ndarray
+ """Vertical conflict entry time [s]."""
+ exit: np.ndarray
+ """Vertical conflict exit time [s]."""
+
def detect(
self,
ownship: Any,
@@ -390,7 +420,7 @@ def detect(
rpz: np.ndarray,
hpz: np.ndarray,
dtlookahead: np.ndarray,
- ) -> tuple:
+ ) -> ConflictDetectionResult:
"""Conflict detection between ownship (traf) and intruder (traf/adsb).
State-based detection with spatial candidate pruning: a KD-tree on
@@ -416,19 +446,6 @@ def detect(
rpz (ndarray): Per-aircraft horizontal separation minimum [m].
hpz (ndarray): Per-aircraft vertical separation minimum [m].
dtlookahead (ndarray): Per-aircraft lookahead time [s].
-
- Returns:
- tuple: The detection results:
- - confpairs (list): Conflicting callsign pairs, both directions.
- - lospairs (list): Callsign pairs in loss of separation.
- - inconf (ndarray): Per-aircraft in-conflict flag [-].
- - tcpamax (ndarray): Per-aircraft maximum tCPA [s].
- - qdr (ndarray): Bearing ownship to intruder per conflict [deg].
- - dist (ndarray): Current horizontal distance per conflict [m].
- - dcpa (ndarray): Horizontal distance at CPA per conflict [m].
- - tcpa (ndarray): Time to CPA per conflict [s].
- - tinconf (ndarray): Time to start of LoS per conflict [s].
- - dalt (ndarray): Current altitude difference per conflict [m].
"""
ntraf = ownship.ntraf
if ntraf < 2:
@@ -522,12 +539,14 @@ def detect(
# flags a conflict). The horizontal geometry is fully symmetric.
dvs = intruder.vs[jj] - ownship.vs[ii]
- def vertical_interval(da: np.ndarray, dw: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
+ def vertical_interval(da: np.ndarray, dw: np.ndarray) -> ConflictDetection.VerticalInterval:
"""Vertical crossing interval of the disk (-hpz, +hpz)."""
dw = np.where(np.abs(dw) < 1e-6, 1e-6, dw) # prevent division by zero
tcrosshi = (da + hpz) / -dw
tcrosslo = (da - hpz) / -dw
- return np.minimum(tcrosshi, tcrosslo), np.maximum(tcrosshi, tcrosslo)
+ return self.VerticalInterval(
+ np.minimum(tcrosshi, tcrosslo), np.maximum(tcrosshi, tcrosslo)
+ )
tinver_ij, toutver_ij = vertical_interval(dalt, dvs)
tinver_ji, toutver_ji = vertical_interval(-dalt, -dvs)
@@ -591,7 +610,7 @@ def vertical_interval(da: np.ndarray, dw: np.ndarray) -> tuple[np.ndarray, np.nd
for i, j in zip(ilos[losorder], jlos[losorder], strict=False)
]
- return (
+ return ConflictDetectionResult(
confpairs,
lospairs,
inconf,
diff --git a/packages/minisky/minisky/traffic/asas/mvp.py b/packages/minisky/minisky/traffic/asas/mvp.py
index d36993b..39eb2aa 100644
--- a/packages/minisky/minisky/traffic/asas/mvp.py
+++ b/packages/minisky/minisky/traffic/asas/mvp.py
@@ -17,14 +17,14 @@
from __future__ import annotations
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
from minisky.core.config import MiniSkyConfig
from minisky.result import Err, Ok, Result
from minisky.stack.argparser import Txt
-from minisky.traffic.asas import ConflictResolution
+from minisky.traffic.asas.resolution import ConflictResolution
if TYPE_CHECKING:
from minisky.traffic import Traffic
@@ -177,6 +177,12 @@ def setresometv(self, value: Txt = "") -> Result[str, str]:
self.swresovert = False
return Ok(f"Vertical resolution method set to {value}")
+ class PriorityResolution(NamedTuple):
+ ownship: np.ndarray
+ """Updated ownship resolution vector [m/s]."""
+ intruder: np.ndarray
+ """Updated intruder resolution vector [m/s]."""
+
def applyprio(
self,
dv_mvp: np.ndarray,
@@ -184,7 +190,7 @@ def applyprio(
dv2: np.ndarray,
vs1: float,
vs2: float,
- ) -> tuple:
+ ) -> PriorityResolution:
"""Apply the desired priority setting to the resolution.
Distributes the pairwise MVP resolution vector over the two aircraft
@@ -200,9 +206,6 @@ def applyprio(
dv2 (ndarray): Accumulated resolution vector of aircraft 2 [m/s].
vs1 (float): Vertical speed of aircraft 1 [m/s].
vs2 (float): Vertical speed of aircraft 2 [m/s].
-
- Returns:
- tuple: Updated (dv1, dv2) resolution vectors [m/s].
"""
# Primary Free Flight prio rules (no priority)
@@ -267,9 +270,11 @@ def applyprio(
dv1 = dv1 - dv_mvp
dv2 = dv2 + dv_mvp
- return dv1, dv2
+ return self.PriorityResolution(dv1, dv2)
- def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
+ def resolve(
+ self, conf: Any, ownship: Any, intruder: Any
+ ) -> ConflictResolution.ResolutionAdvisories:
"""Resolve all current conflicts.
Loops over all detected conflict pairs, computes the MVP resolution
@@ -287,14 +292,6 @@ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
ownship: Traffic object with ownship states.
intruder: Traffic object with intruder states.
- Returns:
- tuple: Per-aircraft advisories:
- - newtrack (ndarray): Resolution track [deg].
- - newgscapped (ndarray): Resolution ground speed, capped to
- the performance envelope [m/s].
- - vscapped (ndarray): Resolution vertical speed, capped to
- the performance envelope [m/s].
- - alt (ndarray): Resolution altitude [m].
"""
# Initialize an array to store the resolution velocity vector for all A/C
dv = np.zeros((ownship.ntraf, 3))
@@ -312,14 +309,18 @@ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
# If A/C indexes are found, then apply MVP on this conflict pair
# Because ADSB is ON, this is done for each aircraft separately
if idx1 > -1 and idx2 > -1:
- dv_mvp, tsolV = self.MVP(ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2)
- timesolveV[idx1] = min(timesolveV[idx1], tsolV)
+ pair_resolution = self.MVP(
+ ownship, intruder, conf, qdr, dist, tcpa, tLOS, idx1, idx2
+ )
+ dv_mvp = pair_resolution.velocity_delta
+ timesolveV[idx1] = min(timesolveV[idx1], pair_resolution.vertical_time)
# Use priority rules if activated
if self.swprio:
- dv[idx1], _ = self.applyprio(
+ priority = self.applyprio(
dv_mvp, dv[idx1], dv[idx2], ownship.vs[idx1], intruder.vs[idx2]
)
+ dv[idx1] = priority.ownship
else:
# since cooperative, the vertical resolution component can be halved, and then dv_mvp can be added
dv_mvp[2] = 0.5 * dv_mvp[2]
@@ -399,7 +400,13 @@ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
# using the auto pilot vertical speed (ownship.avs) using the code in line 106 (asasalttemp) when only
# horizontal resolutions are allowed.
alt = alt * (1 - self.swresohoriz) + ownship.selalt * self.swresohoriz
- return newtrack, newgscapped, vscapped, alt
+ return self.ResolutionAdvisories(newtrack, newgscapped, vscapped, alt)
+
+ class MvpResolution(NamedTuple):
+ velocity_delta: np.ndarray
+ """Resolution velocity change, east/north/up [m/s]."""
+ vertical_time: float
+ """Time needed to resolve the conflict vertically [s]."""
def MVP(
self,
@@ -412,7 +419,7 @@ def MVP(
tLOS: float,
idx1: int,
idx2: int,
- ) -> tuple:
+ ) -> MvpResolution:
"""Modified Voltage Potential (MVP) resolution method.
Computes the velocity change that displaces the predicted closest
@@ -438,11 +445,6 @@ def MVP(
tLOS (float): Time until loss of separation starts [s].
idx1 (int): Index of the ownship aircraft.
idx2 (int): Index of the intruder aircraft.
-
- Returns:
- tuple: (dv, tsolV) where dv is the resolution velocity change
- (east, north, up) [m/s] and tsolV the time needed to resolve
- the conflict vertically [s].
"""
# Preliminary calculations-------------------------------------------------
# Determine largest RPZ and HPZ of the conflict pair, use lookahead of ownship
@@ -528,4 +530,4 @@ def MVP(
# combine the dv components
dv = np.array([dv1, dv2, dv3])
- return dv, tsolV
+ return self.MvpResolution(dv, tsolV)
diff --git a/packages/minisky/minisky/traffic/asas/resolution.py b/packages/minisky/minisky/traffic/asas/resolution.py
index d33b071..b7dcdb1 100644
--- a/packages/minisky/minisky/traffic/asas/resolution.py
+++ b/packages/minisky/minisky/traffic/asas/resolution.py
@@ -16,7 +16,7 @@
from __future__ import annotations
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
@@ -171,7 +171,17 @@ def tasactive(self) -> np.ndarray:
"""
return self.active
- def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
+ class ResolutionAdvisories(NamedTuple):
+ track: np.ndarray
+ """Per-aircraft track advisory [deg]."""
+ tas: np.ndarray
+ """Per-aircraft true airspeed advisory [m/s]."""
+ vertical_speed: np.ndarray
+ """Per-aircraft vertical speed advisory [m/s]."""
+ altitude: np.ndarray
+ """Per-aircraft altitude advisory [m]."""
+
+ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> ResolutionAdvisories:
"""Resolve all current conflicts.
This function should be reimplemented in a subclass for actual
@@ -183,15 +193,13 @@ def resolve(self, conf: Any, ownship: Any, intruder: Any) -> tuple:
conf: The ConflictDetection instance with the current conflicts.
ownship: Traffic object with ownship states.
intruder: Traffic object with intruder states.
-
- Returns:
- tuple: Per-aircraft advisories (newtrk [deg], newtas [m/s],
- newvs [m/s], newalt [m]).
"""
# If resolution is off, and detection is on, and a conflict is detected
# then asas will be active for that airplane. Since resolution is off, it
# should then follow the auto pilot instructions.
- return ownship.ap.trk, ownship.ap.tas, ownship.ap.vs, ownship.ap.alt
+ return self.ResolutionAdvisories(
+ ownship.ap.trk, ownship.ap.tas, ownship.ap.vs, ownship.ap.alt
+ )
def update(self, conf: Any, ownship: Any, intruder: Any) -> None:
"""Perform an update step of the Conflict Resolution implementation.
@@ -207,7 +215,12 @@ def update(self, conf: Any, ownship: Any, intruder: Any) -> None:
"""
if self.activate:
if conf.confpairs:
- self.trk, self.tas, self.vs, self.alt = self.resolve(conf, ownship, intruder)
+ advisories = self.resolve(conf, ownship, intruder)
+ # TODO(abraham): consider storing the entire advisories result
+ self.trk = advisories.track
+ self.tas = advisories.tas
+ self.vs = advisories.vertical_speed
+ self.alt = advisories.altitude
self.resumenav(conf, ownship, intruder)
def resumenav(self, conf: Any, ownship: Any, intruder: Any) -> None:
diff --git a/packages/minisky/minisky/traffic/performance/perfoap.py b/packages/minisky/minisky/traffic/performance/perfoap.py
index 9c3bceb..f793e51 100644
--- a/packages/minisky/minisky/traffic/performance/perfoap.py
+++ b/packages/minisky/minisky/traffic/performance/perfoap.py
@@ -10,7 +10,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, NamedTuple
import numpy as np
@@ -344,13 +344,21 @@ def update(self, dt: float = 1) -> None:
self.bank,
)
+ class PerformanceLimits(NamedTuple):
+ tas: np.ndarray
+ """Allowed true airspeed [m/s]."""
+ vertical_speed: np.ndarray
+ """Allowed vertical speed [m/s]."""
+ altitude: np.ndarray
+ """Allowed altitude [m]."""
+
def limits(
self,
intent_v_tas: np.ndarray,
intent_vs: np.ndarray,
intent_h: np.ndarray,
ax: np.ndarray,
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ ) -> PerformanceLimits:
"""apply limits on indent speed, vertical speed, and altitude (called in pilot module)
Clips the intended state to the aircraft flight envelope: altitude to
@@ -365,10 +373,6 @@ def limits(
intent_vs (float or 1D-array): intent vertical speed [m/s]
intent_h (float or 1D-array): intent altitude [m]
ax (float or 1D-array): acceleration [m/s^2]
-
- Returns:
- floats or 1D-arrays: Allowed TAS [m/s], Allowed vertical
- rate [m/s], Allowed altitude [m]
"""
allow_h = np.where(intent_h > self.hmax, self.hmax, intent_h)
@@ -405,9 +409,20 @@ def limits(
allow_vs[ir] = np.where((intent_vs[ir] < self.vsmin[ir]), self.vsmin[ir], intent_vs[ir])
allow_vs[ir] = np.where((intent_vs[ir] > self.vsmax[ir]), self.vsmax[ir], allow_vs[ir])
- return allow_v_tas, allow_vs, allow_h
+ return self.PerformanceLimits(allow_v_tas, allow_vs, allow_h)
- def currentlimits(self, id: Any = None) -> tuple:
+ # TODO(abraham): maybe make this Generic over float/np/any array?
+ class CurrentPerformanceLimits(NamedTuple):
+ minimum_tas: float | np.ndarray
+ """Minimum true airspeed [m/s]."""
+ maximum_tas: float | np.ndarray
+ """Maximum true airspeed [m/s]."""
+ minimum_vertical_speed: float | np.ndarray
+ """Minimum vertical speed [m/s]."""
+ maximum_vertical_speed: float | np.ndarray
+ """Maximum vertical speed [m/s]."""
+
+ def currentlimits(self, id: Any = None) -> CurrentPerformanceLimits:
"""Get current kinematic performance envelop.
Converts the phase-dependent CAS limits to TAS at the current
@@ -416,10 +431,6 @@ def currentlimits(self, id: Any = None) -> tuple:
Args:
id (int or 1D-array): Aircraft ID(s). Defualt to None (all aircraft).
-
- Returns:
- floats or 1D-arrays: Min TAS [m/s], Max TAS [m/s],
- Min VS [m/s], Max VS [m/s]
"""
vtasmin = aero.vcas2tas(self.vmin, self.traffic.alt)
@@ -429,11 +440,18 @@ def currentlimits(self, id: Any = None) -> tuple:
)
if id is not None:
- return vtasmin[id], vtasmax[id], self.vsmin[id], self.vsmax[id]
- else:
- return vtasmin, vtasmax, self.vsmin, self.vsmax
+ return self.CurrentPerformanceLimits(
+ vtasmin[id], vtasmax[id], self.vsmin[id], self.vsmax[id]
+ )
+ return self.CurrentPerformanceLimits(vtasmin, vtasmax, self.vsmin, self.vsmax)
+
+ class SpeedLimits(NamedTuple):
+ minimum: np.ndarray
+ """Minimum calibrated airspeed [m/s]."""
+ maximum: np.ndarray
+ """Maximum calibrated airspeed [m/s]."""
- def _construct_v_limits(self, mask: Any = True) -> tuple[np.ndarray, np.ndarray]:
+ def _construct_v_limits(self, mask: Any = True) -> SpeedLimits:
"""Compute speed limist base on aircraft model and flight phases
For fixed-wing aircraft the applicable minimum and maximum calibrated
@@ -443,9 +461,6 @@ def _construct_v_limits(self, mask: Any = True) -> tuple[np.ndarray, np.ndarray]
Args:
mask: Indices (boolean) for aircraft to construct speed limits for.
When no indices are passed, all aircraft are updated.
-
- Returns:
- 2D-array: vmin, vmax (CAS limits per aircraft [m/s])
"""
n = len(self.actype)
vmin = np.zeros(n)
@@ -491,8 +506,8 @@ def _construct_v_limits(self, mask: Any = True) -> tuple[np.ndarray, np.ndarray]
vmax[ir] = vmaxr
if isinstance(mask, bool):
- return vmin, vmax
- return vmin[mask], vmax[mask]
+ return self.SpeedLimits(vmin, vmax)
+ return self.SpeedLimits(vmin[mask], vmax[mask])
def calc_axmax(self) -> np.ndarray:
"""Compute the maximum longitudinal acceleration per aircraft.
diff --git a/packages/minisky/minisky/traffic/performance/thrust.py b/packages/minisky/minisky/traffic/performance/thrust.py
index 742bd40..0454709 100644
--- a/packages/minisky/minisky/traffic/performance/thrust.py
+++ b/packages/minisky/minisky/traffic/performance/thrust.py
@@ -8,7 +8,7 @@
engine emission databank fuel-flow points as a function of thrust ratio.
"""
-from typing import Any
+from typing import NamedTuple
import numpy as np
@@ -170,9 +170,18 @@ def mfunc(vratio, roc):
return ratio_F0
+class FuelFlowCoefficients(NamedTuple):
+ quadratic: float | np.ndarray
+ """Quadratic fuel-flow coefficient [kg/s]."""
+ linear: float | np.ndarray
+ """Linear fuel-flow coefficient [kg/s]."""
+ constant: float | np.ndarray
+ """Constant fuel-flow coefficient [kg/s]."""
+
+
def compute_eng_ff_coeff(
ffidl: float, ffapp: float, ffco: float, ffto: float
-) -> tuple[Any, Any, Any]:
+) -> FuelFlowCoefficients:
"""Compute fuel flow based on engine icao fuel flow model
Fits a quadratic polynomial through the four fuel-flow measurement
@@ -185,9 +194,6 @@ def compute_eng_ff_coeff(
ffapp (float or 1D-array): fuel flow at approach thrust (30%) [kg/s]
ffco (float or 1D-array): fuel flow at climb-out thrust (85%) [kg/s]
ffto (float or 1D-array): fuel flow at takeoff thrust (100%) [kg/s]
-
- Returns:
- list of coeff: [a, b, c], fuel flow calc: ax^2 + bx + c
"""
# standard fuel flow at test thrust ratios
@@ -196,4 +202,4 @@ def compute_eng_ff_coeff(
a, b, c = np.polyfit(x, y, 2)
- return a, b, c
+ return FuelFlowCoefficients(a, b, c)
diff --git a/packages/minisky/minisky/traffic/route.py b/packages/minisky/minisky/traffic/route.py
index 82001f9..2a998c6 100644
--- a/packages/minisky/minisky/traffic/route.py
+++ b/packages/minisky/minisky/traffic/route.py
@@ -16,7 +16,7 @@
from __future__ import annotations
import math
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, NamedTuple
import numpy as np
@@ -390,7 +390,46 @@ def getnextturnwp(self) -> list:
trnidx,
]
- def getnextwp(self) -> tuple:
+ # TODO(abraham): split this large transition record into constraints, turn,
+ # and next-leg records
+ # TODO(abraham): replace -999.0 sentinels with explicit optional/validity state (see issue #40)
+ class WaypointTransition(NamedTuple):
+ latitude: float
+ """Active waypoint latitude [deg]."""
+ longitude: float
+ """Active waypoint longitude [deg]."""
+ altitude: float
+ """Altitude constraint [m]."""
+ speed: float
+ """Speed constraint, calibrated airspeed [m/s] or Mach number [-]."""
+ distance_to_altitude: float
+ """Distance to the next altitude constraint [m]."""
+ next_altitude: float
+ """Next altitude constraint [m]."""
+ distance_to_rta: float
+ """Distance to the next required time of arrival [m]."""
+ next_rta: float
+ """Next required time of arrival [s]."""
+ lnav_enabled: bool
+ """Whether lateral navigation remains enabled."""
+ fly_by: bool
+ """Whether the waypoint uses fly-by switching."""
+ fly_turn: bool
+ """Whether the waypoint uses an explicit turn."""
+ turn_radius: float
+ """Turn radius [m]."""
+ turn_speed: float
+ """Turn calibrated airspeed [m/s]."""
+ turn_heading_rate: float
+ """Turn heading rate [deg/s]."""
+ next_leg_latitude: float
+ """Next-leg endpoint latitude [deg], or -999.0 when there is no next leg."""
+ next_leg_longitude: float
+ """Next-leg endpoint longitude [deg], or -999.0 when there is no next leg."""
+ last_waypoint: bool
+ """Whether this is the final waypoint."""
+
+ def getnextwp(self) -> WaypointTransition:
"""Activate the next waypoint in the route and return its data.
Called by the autopilot when the active waypoint has been passed.
@@ -399,16 +438,6 @@ def getnextwp(self) -> tuple:
a runway used for landing, a fixed runway heading is commanded and
deceleration plus deletion of the aircraft are scheduled via the
stack.
-
- Returns:
- tuple: (lat [deg], lon [deg], altitude constraint [m], speed
- constraint (CAS [m/s] or Mach), distance to next altitude
- constraint [m], next altitude constraint [m], distance to next
- RTA [m], next RTA [s], lnavon switch, fly-by switch, fly-turn
- switch, turn radius, turn speed (CAS), turn heading rate
- [deg/s], next-leg endpoint lat [deg], next-leg endpoint lon
- [deg] (-999.0 pair when there is no next leg), last-waypoint
- switch).
"""
n_wpt = len(self.wpname)
@@ -451,7 +480,7 @@ def getnextwp(self) -> tuple:
swlastwp = self.iactwp == n_wpt - 1
- return (
+ return self.WaypointTransition(
self.wplat[self.iactwp],
self.wplon[self.iactwp],
self.wpalt[self.iactwp],
@@ -504,7 +533,7 @@ def getnextwp(self) -> tuple:
# print ("getnextwp:",self.wpname[self.iactwp]," torta = ",self.wptorta[self.iactwp])
- return (
+ return self.WaypointTransition(
self.wplat[self.iactwp],
self.wplon[self.iactwp],
self.wpalt[self.iactwp],
From 78527c5fe163366f4736f1579ba9a56ac18d0a76 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 15:49:47 +0800
Subject: [PATCH 7/8] style: more ruff lint fixes
---
packages/minisky/minisky/cli.py | 12 ++++----
packages/minisky/minisky/plugin/plugin.py | 2 +-
packages/minisky/minisky/server.py | 10 +++----
packages/minisky/minisky/stack/__init__.py | 13 ++++-----
packages/minisky/minisky/tools/navdata.py | 4 +--
.../minisky/traffic/performance/coeff.py | 2 +-
.../minisky/traffic/performance/perfoap.py | 8 +++---
packages/minisky/minisky/traffic/route.py | 4 +--
pyproject.toml | 28 ++++++++++++-------
tests/integration/test_stack.py | 6 ++--
tests/unit/test_convert.py | 6 ++--
tests/unit/test_detection.py | 9 ++++--
tests/unit/test_geo.py | 2 +-
13 files changed, 58 insertions(+), 48 deletions(-)
diff --git a/packages/minisky/minisky/cli.py b/packages/minisky/minisky/cli.py
index 402304f..714b2dd 100644
--- a/packages/minisky/minisky/cli.py
+++ b/packages/minisky/minisky/cli.py
@@ -31,7 +31,7 @@
typer.Option(help="Config TOML file. Overrides the default user config path."),
]
-history_file = os.path.expanduser("/tmp/hacksky_console_history")
+history_file = Path("/tmp/hacksky_console_history").expanduser()
path_completer = PathCompleter()
completer = NestedCompleter.from_nested_dict({"load": path_completer, "/load": path_completer})
@@ -131,7 +131,7 @@ def console_cmd(
while True:
print(Fore.LIGHTGREEN_EX + Style.BRIGHT, end="")
- cmd = prompt("> ", completer=completer, history=FileHistory(history_file))
+ cmd = prompt("> ", completer=completer, history=FileHistory(str(history_file)))
print(Style.RESET_ALL, end="")
if cmd == "":
@@ -145,11 +145,11 @@ def console_cmd(
continue
if cmd.startswith(("/load ", "load ")):
- file_path = cmd.split(" ", maxsplit=1)[1]
+ file_path = Path(cmd.split(" ", maxsplit=1)[1])
- if os.path.isfile(file_path):
- with open(file_path, "rb") as f:
- files = {"file": (os.path.basename(file_path), f)}
+ if file_path.is_file():
+ with file_path.open("rb") as f:
+ files = {"file": (file_path.name, f)}
response = requests.post(f"{root_url}/scn", files=files, timeout=30)
typer.echo(response.json())
else:
diff --git a/packages/minisky/minisky/plugin/plugin.py b/packages/minisky/minisky/plugin/plugin.py
index 9a97299..b5439bb 100644
--- a/packages/minisky/minisky/plugin/plugin.py
+++ b/packages/minisky/minisky/plugin/plugin.py
@@ -422,7 +422,7 @@ def _prepare(self, key: str, spec: PluginSpec) -> _PreparedPlugin:
aliases=bound.aliases,
arguments=bound.declaration.arguments,
brief=bound.brief,
- help=bound.help,
+ help_text=bound.help,
)
overlap = command_names.intersection(prepared.names)
if overlap:
diff --git a/packages/minisky/minisky/server.py b/packages/minisky/minisky/server.py
index 2099841..9f2eb82 100644
--- a/packages/minisky/minisky/server.py
+++ b/packages/minisky/minisky/server.py
@@ -22,9 +22,9 @@
from __future__ import annotations
import asyncio
-import os
from contextlib import asynccontextmanager, suppress
from io import StringIO
+from pathlib import Path
from typing import Annotated, Any, Literal, TypeAlias, TypedDict, cast
import pandas as pd
@@ -120,8 +120,8 @@ def create_app(runtime: MiniSky | None = None) -> FastAPI:
# TODO(abraham): package static assets inside minisky and resolve them
# with importlib.resources for wheel installs
- static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
- os.makedirs(static_dir, exist_ok=True)
+ static_dir = Path(__file__).parent.parent / "static"
+ static_dir.mkdir(parents=True, exist_ok=True)
app.mount("/static", StaticFiles(directory=static_dir), name="static")
return app
@@ -131,7 +131,7 @@ def root() -> dict[str, str]:
return {"msg": "MiniSky API endpoint ready"}
-def all(runtime: Runtime) -> list[dict[str, Any]]:
+def all_aircraft(runtime: Runtime) -> list[dict[str, Any]]:
"""Get all aircraft states."""
traffic = runtime.traffic
df = pd.DataFrame(
@@ -305,7 +305,7 @@ def create_router() -> APIRouter:
"""Create the API router for a FastAPI application."""
router = APIRouter()
router.add_api_route("/", root, methods=["GET"])
- router.add_api_route("/all", all, methods=["GET"])
+ router.add_api_route("/all", all_aircraft, methods=["GET"])
router.add_api_route("/simtime", simtime, methods=["GET"])
router.add_api_route("/speed/{speed}", speedup, methods=["GET"])
router.add_api_route("/forward/{seconds}", forward, methods=["GET"])
diff --git a/packages/minisky/minisky/stack/__init__.py b/packages/minisky/minisky/stack/__init__.py
index 5728e27..dec041e 100644
--- a/packages/minisky/minisky/stack/__init__.py
+++ b/packages/minisky/minisky/stack/__init__.py
@@ -25,7 +25,6 @@
import asyncio
import inspect
-import os
import traceback
from collections.abc import Awaitable, Callable, Iterator
from contextlib import suppress
@@ -221,7 +220,7 @@ def helptext(self, subcmd: str = "") -> str:
msg += f"\nFunction {self._callback_source.__name__}(), implemented in "
if hasattr(self._callback_source, "__code__"):
fname = self._callback_source.__code__.co_filename
- fname_stripped = fname.replace(os.getcwd(), "").lstrip("/")
+ fname_stripped = fname.replace(str(Path.cwd()), "").lstrip("/")
firstline = self._callback_source.__code__.co_firstlineno
msg += f"{fname_stripped} on line {firstline}"
else:
@@ -351,7 +350,7 @@ def prepare_command(
aliases: tuple[str, ...] = (),
arguments: str = "",
brief: str = "",
- help: str = "",
+ help_text: str = "",
) -> PreparedCommand:
"""Construct and parse a command without registering it."""
callback = func.__func__ if isinstance(func, (staticmethod, classmethod)) else func
@@ -368,7 +367,7 @@ def prepare_command(
aliases=alias_names,
arguments=arguments,
brief=brief,
- help=help,
+ help=help_text,
)
return PreparedCommand(command_obj, names)
@@ -438,7 +437,7 @@ def init(self) -> None:
aliases=catalog.aliases.get(name, ()),
arguments=definition.arguments,
brief=definition.brief,
- help=definition.help,
+ help_text=definition.help,
)
for name, definition in catalog.definitions.items()
)
@@ -628,7 +627,7 @@ def readscn(self, scn: str | Path | StringIO) -> Iterator[tuple[float, str]]:
# ensure .scn suffix if necessary
scn_path = Path(scn).with_suffix(".scn")
- with open(scn_path) as fscen:
+ with scn_path.open() as fscen:
scn_input = StringIO(fscen.read())
elif isinstance(scn, StringIO):
scn_input = scn
@@ -798,7 +797,7 @@ def showhelp(self, cmd: Txt = "", subcmd: Txt = "") -> Result[str, str]:
# Sort & write table
table.sort()
- with open(fname, "w") as f:
+ with Path(fname).open("w") as f:
# Header of first table
f.write("Command\tDescription\tUsage\tArgument types\tFunction\tSynonyms\n")
f.write("\n".join(table))
diff --git a/packages/minisky/minisky/tools/navdata.py b/packages/minisky/minisky/tools/navdata.py
index 5661759..042a2ff 100644
--- a/packages/minisky/minisky/tools/navdata.py
+++ b/packages/minisky/minisky/tools/navdata.py
@@ -126,9 +126,9 @@ def reset(self) -> None:
awydata = pd.read_parquet(nav_data_path / "airway.parquet")
codata = pd.read_parquet(nav_data_path / "country.parquet")
- with open(nav_data_path / "fir.json") as f:
+ with (nav_data_path / "fir.json").open() as f:
firdata = json.load(f)
- with open(nav_data_path / "runway_thresholds.json") as f:
+ with (nav_data_path / "runway_thresholds.json").open() as f:
rwythresholds = json.load(f)
# Get waypoint data
diff --git a/packages/minisky/minisky/traffic/performance/coeff.py b/packages/minisky/minisky/traffic/performance/coeff.py
index 5207a5d..7108bba 100644
--- a/packages/minisky/minisky/traffic/performance/coeff.py
+++ b/packages/minisky/minisky/traffic/performance/coeff.py
@@ -84,7 +84,7 @@ def _load_all_fixwing_flavor(self) -> dict:
def _load_all_rotor_flavor(self) -> dict:
"""Load rotorcraft data from the local JSON database."""
# read rotor aircraft
- with open(OPENAP_DIR / "rotor/aircraft.json") as f:
+ with (OPENAP_DIR / "rotor/aircraft.json").open() as f:
acs = json.load(f)
acs.pop("__comment")
acs_ = {}
diff --git a/packages/minisky/minisky/traffic/performance/perfoap.py b/packages/minisky/minisky/traffic/performance/perfoap.py
index f793e51..d0276b5 100644
--- a/packages/minisky/minisky/traffic/performance/perfoap.py
+++ b/packages/minisky/minisky/traffic/performance/perfoap.py
@@ -422,7 +422,7 @@ class CurrentPerformanceLimits(NamedTuple):
maximum_vertical_speed: float | np.ndarray
"""Maximum vertical speed [m/s]."""
- def currentlimits(self, id: Any = None) -> CurrentPerformanceLimits:
+ def currentlimits(self, idx: Any = None) -> CurrentPerformanceLimits:
"""Get current kinematic performance envelop.
Converts the phase-dependent CAS limits to TAS at the current
@@ -430,7 +430,7 @@ def currentlimits(self, id: Any = None) -> CurrentPerformanceLimits:
operating Mach number.
Args:
- id (int or 1D-array): Aircraft ID(s). Defualt to None (all aircraft).
+ idx (int or 1D-array): Aircraft index or indices. Defaults to all aircraft.
"""
vtasmin = aero.vcas2tas(self.vmin, self.traffic.alt)
@@ -439,9 +439,9 @@ def currentlimits(self, id: Any = None) -> CurrentPerformanceLimits:
aero.vmach2tas(self.mmo, self.traffic.alt),
)
- if id is not None:
+ if idx is not None:
return self.CurrentPerformanceLimits(
- vtasmin[id], vtasmax[id], self.vsmin[id], self.vsmax[id]
+ vtasmin[idx], vtasmax[idx], self.vsmin[idx], self.vsmax[idx]
)
return self.CurrentPerformanceLimits(vtasmin, vtasmax, self.vsmin, self.vsmax)
diff --git a/packages/minisky/minisky/traffic/route.py b/packages/minisky/minisky/traffic/route.py
index 2a998c6..d634c3f 100644
--- a/packages/minisky/minisky/traffic/route.py
+++ b/packages/minisky/minisky/traffic/route.py
@@ -643,7 +643,7 @@ def calcfp(self) -> None:
qdr, dist = geo.qdrdist(
self.traffic.lat[iac], self.traffic.lon[iac], self.wplat[0], self.wplon[0]
)
- self.wpdirto = [qdr] + self.wpdirfrom[0:-1] # [deg] Direction to waypoints
+ self.wpdirto = [qdr, *self.wpdirfrom[0:-1]] # [deg] Direction to waypoints
# Continue flying in the saem direction
if n_wpt > 1:
@@ -1086,7 +1086,7 @@ def addwpt(
rwylon = traffic.lon[acidx]
rwyhdg = traffic.trk[acidx]
- elif args[1].count("/") > 0 or len(args) > 2 and args[2]: # we need apt,rwy
+ elif args[1].count("/") > 0 or (len(args) > 2 and args[2]): # we need apt,rwy
# Take care of both EHAM/RW06 as well as EHAM,RWY18L (so /&, and RW/RWY)
if args[1].count("/") > 0:
aptid, rwyname = args[1].split("/")
diff --git a/pyproject.toml b/pyproject.toml
index 9aa8876..24844c7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,25 +9,27 @@ dev = [
"pyright>=1.1.390",
"fakeredis>=2.26",
]
-docs = [
- "zensical>=0.0.47",
- "mkdocstrings-python>=2.0.5",
-]
+docs = ["zensical>=0.0.47", "mkdocstrings-python>=2.0.5"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = [".", "packages/minisky"]
addopts = "-ra -m 'not api'"
-markers = [
- "api: FastAPI endpoint tests, run separately with just test-api",
-]
+markers = ["api: FastAPI endpoint tests, run separately with just test-api"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
-extend-select = ["NPY"]
+extend-select = [
+ "NPY",
+ "PT",
+ "A", # shadowing
+ "PTH", # prefer pathlib over os.path and built-in open()
+ "RUF006", # keep references to created asyncio tasks
+ "RUF021", # parenthesize mixed and/or expressions
+]
[tool.pyright]
include = [
@@ -38,9 +40,15 @@ include = [
"tests",
]
exclude = [
- "**/.*", "**/__pycache__", "**/node_modules", ".venv", "build", "site",
+ "**/.*",
+ "**/__pycache__",
+ "**/node_modules",
+ ".venv",
+ "build",
+ "site",
# TODO(abraham): these files will need extensive typing, deferring for now
- "tests/unit/test_aero.py", "tests/unit/test_detection.py"
+ "tests/unit/test_aero.py",
+ "tests/unit/test_detection.py",
]
extraPaths = [
"packages/minisky",
diff --git a/tests/integration/test_stack.py b/tests/integration/test_stack.py
index 4f2f9f2..1b38fb4 100644
--- a/tests/integration/test_stack.py
+++ b/tests/integration/test_stack.py
@@ -223,9 +223,9 @@ def test_all_registered_specs_resolve_to_parsers(self, runtime: MiniSky) -> None
continue
seen.add(id(cmd))
for annot, _isopt in cmd.arguments:
- assert annot == annot.strip() and annot, (
- f"{cmd.name}: whitespace/empty annotation token {annot!r}"
- )
+ message = f"{cmd.name}: whitespace/empty annotation token {annot!r}"
+ assert annot == annot.strip(), message
+ assert annot, message
if annot in placeholders:
continue
if all(argparsers.get(part) is None for part in annot.split("/")):
diff --git a/tests/unit/test_convert.py b/tests/unit/test_convert.py
index 91d9d6c..8671f85 100644
--- a/tests/unit/test_convert.py
+++ b/tests/unit/test_convert.py
@@ -19,7 +19,7 @@ def test_plain_feet(self) -> None:
assert cv.txt2alt("2500") == pytest.approx(2500 * FT)
def test_invalid_raises(self) -> None:
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r'Could not parse "NOTANALT" as altitude'):
cv.txt2alt("NOTANALT")
@@ -63,13 +63,13 @@ def test_mach_passthrough(self) -> None:
assert cv.txt2spd(".8") == pytest.approx(0.8)
def test_invalid_raises(self) -> None:
- with pytest.raises(ValueError):
+ with pytest.raises(ValueError, match=r"Could not parse FAST as speed\."):
cv.txt2spd("FAST")
class TestAngles:
@pytest.mark.parametrize(
- "angle,expected",
+ ("angle", "expected"),
[(190.0, -170.0), (-190.0, 170.0), (180.0, -180.0), (0.0, 0.0), (359.0, -1.0)],
)
def test_degto180_wraps(self, angle: float, expected: float) -> None:
diff --git a/tests/unit/test_detection.py b/tests/unit/test_detection.py
index b35ef81..f52658c 100644
--- a/tests/unit/test_detection.py
+++ b/tests/unit/test_detection.py
@@ -226,12 +226,15 @@ def test_no_traffic(self):
traf = make_traffic(0, 9)
cd = ConflictDetection.__new__(ConflictDetection)
result = cd.detect(traf, traf, *default_params(0))
- assert result[0] == [] and result[1] == []
- assert len(result[2]) == 0 and len(result[3]) == 0
+ assert result[0] == []
+ assert result[1] == []
+ assert len(result[2]) == 0
+ assert len(result[3]) == 0
def test_single_aircraft(self):
traf = make_traffic(1, 10)
cd = ConflictDetection.__new__(ConflictDetection)
result = cd.detect(traf, traf, *default_params(1))
- assert result[0] == [] and result[1] == []
+ assert result[0] == []
+ assert result[1] == []
assert not result[2].any()
diff --git a/tests/unit/test_geo.py b/tests/unit/test_geo.py
index dd18851..6317eb1 100644
--- a/tests/unit/test_geo.py
+++ b/tests/unit/test_geo.py
@@ -95,7 +95,7 @@ def test_qdrdist_matrix_matches_scalar(self) -> None:
class TestProjection:
- @pytest.mark.parametrize("qdr,dist", [(0.0, 60.0), (45.0, 100.0), (270.0, 30.0)])
+ @pytest.mark.parametrize(("qdr", "dist"), [(0.0, 60.0), (45.0, 100.0), (270.0, 30.0)])
def test_qdrpos_roundtrip(self, qdr: float, dist: float) -> None:
lat2, lon2 = geo.qdrpos(52.0, 4.0, qdr, dist)
qdr_back, dist_back = geo.qdrdist(52.0, 4.0, lat2, lon2)
From 03d9f7df44938a20ee4748634cd32cf86aa80013 Mon Sep 17 00:00:00 2001
From: Abraham Cheung <58929011+abc8747@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:16:52 +0800
Subject: [PATCH 8/8] test: move top level tests into respective core/plugins
---
.github/workflows/tests.yml | 6 +-
justfile | 12 ++--
.../tests/test_customautopilot_plugin.py | 30 ++++++++
.../tests/test_example_plugin.py | 50 ++++++++++++++
packages/minisky-tangram/tests/conftest.py | 35 ++++++++++
.../tests}/integration/test_tangram_bridge.py | 0
.../tests}/unit/test_tangram_plugin.py | 0
{tests => packages/minisky/tests}/_types.py | 0
{tests => packages/minisky/tests}/conftest.py | 1 -
.../tests}/integration/test_conflict.py | 1 -
.../tests}/integration/test_navdata.py | 0
.../minisky/tests}/integration/test_plugin.py | 68 -------------------
.../integration/test_route_autopilot.py | 1 -
.../tests}/integration/test_scenario.py | 1 -
.../minisky/tests}/integration/test_stack.py | 1 -
.../tests}/integration/test_streaming.py | 1 -
.../tests}/integration/test_traffic.py | 1 -
{tests => packages/minisky/tests}/test_api.py | 0
.../minisky/tests}/unit/test_aero.py | 0
.../minisky/tests}/unit/test_areafilter.py | 0
.../minisky/tests}/unit/test_convert.py | 0
.../minisky/tests}/unit/test_detection.py | 0
.../minisky/tests}/unit/test_geo.py | 0
.../minisky/tests}/unit/test_phase.py | 0
pyproject.toml | 13 ++--
25 files changed, 133 insertions(+), 88 deletions(-)
create mode 100644 packages/minisky-example-customautopilot/tests/test_customautopilot_plugin.py
create mode 100644 packages/minisky-example/tests/test_example_plugin.py
create mode 100644 packages/minisky-tangram/tests/conftest.py
rename {tests => packages/minisky-tangram/tests}/integration/test_tangram_bridge.py (100%)
rename {tests => packages/minisky-tangram/tests}/unit/test_tangram_plugin.py (100%)
rename {tests => packages/minisky/tests}/_types.py (100%)
rename {tests => packages/minisky/tests}/conftest.py (99%)
rename {tests => packages/minisky/tests}/integration/test_conflict.py (99%)
rename {tests => packages/minisky/tests}/integration/test_navdata.py (100%)
rename {tests => packages/minisky/tests}/integration/test_plugin.py (86%)
rename {tests => packages/minisky/tests}/integration/test_route_autopilot.py (99%)
rename {tests => packages/minisky/tests}/integration/test_scenario.py (99%)
rename {tests => packages/minisky/tests}/integration/test_stack.py (99%)
rename {tests => packages/minisky/tests}/integration/test_streaming.py (99%)
rename {tests => packages/minisky/tests}/integration/test_traffic.py (99%)
rename {tests => packages/minisky/tests}/test_api.py (100%)
rename {tests => packages/minisky/tests}/unit/test_aero.py (100%)
rename {tests => packages/minisky/tests}/unit/test_areafilter.py (100%)
rename {tests => packages/minisky/tests}/unit/test_convert.py (100%)
rename {tests => packages/minisky/tests}/unit/test_detection.py (100%)
rename {tests => packages/minisky/tests}/unit/test_geo.py (100%)
rename {tests => packages/minisky/tests}/unit/test_phase.py (100%)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e9ea7b8..50a6aa9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -50,12 +50,12 @@ jobs:
- name: Check code quality
run: |
- uv run ruff check packages tests
- uv run ruff format packages tests --check
+ uv run ruff check packages
+ uv run ruff format packages --check
uv run pyright
- name: Run unit and integration tests
run: uv run pytest
- name: Run API smoke tests
- run: uv run pytest -m api tests/test_api.py
+ run: uv run pytest -m api packages/minisky/tests/test_api.py
diff --git a/justfile b/justfile
index 12dcb08..8584e59 100644
--- a/justfile
+++ b/justfile
@@ -4,13 +4,13 @@ sync:
uv sync --all-packages
fmt:
- uv run ruff check packages tests --fix
- uv run ruff format packages tests
+ uv run ruff check packages --fix
+ uv run ruff format packages
pnpm lint:fix
check:
- uv run ruff check packages tests
- uv run ruff format packages tests --check
+ uv run ruff check packages
+ uv run ruff format packages --check
uv run pyright
pnpm check
@@ -20,11 +20,11 @@ test:
# Run fast unit tests.
test-unit:
- uv run pytest tests/unit
+ uv run pytest packages/*/tests/unit
# Run opt-in REST API tests.
test-api:
- uv run pytest -m api tests/test_api.py
+ uv run pytest -m api packages/minisky/tests/test_api.py
docs-serve:
uv run --group docs zensical serve
diff --git a/packages/minisky-example-customautopilot/tests/test_customautopilot_plugin.py b/packages/minisky-example-customautopilot/tests/test_customautopilot_plugin.py
new file mode 100644
index 0000000..080cf74
--- /dev/null
+++ b/packages/minisky-example-customautopilot/tests/test_customautopilot_plugin.py
@@ -0,0 +1,30 @@
+"""Integration tests owned by the custom-autopilot example package."""
+
+import pytest
+from minisky import MiniSky, MiniSkyConfig
+from minisky.traffic.autopilot import Autopilot
+from minisky_example_customautopilot import CustomAutoPilot
+
+
+@pytest.mark.anyio
+async def test_replacement_is_runtime_local_and_removed_on_shutdown() -> None:
+ runtime_a = MiniSky(MiniSkyConfig())
+ runtime_b = MiniSky(MiniSkyConfig())
+ try:
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
+ assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
+
+ alt_callback = runtime_a.commands.cmddict["ALT"].callback
+ result = await runtime_a.plugins.load("CUSTOMAUTOPILOT")
+ assert result.is_ok(), result.err()
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_ok()
+ assert type(runtime_a.traffic.ap) is CustomAutoPilot
+ assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
+
+ await runtime_a.plugins.aclose()
+ assert type(runtime_a.traffic.ap) is Autopilot
+ assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
+ assert runtime_a.commands.cmddict["ALT"].callback is alt_callback
+ finally:
+ await runtime_a.aclose()
+ await runtime_b.aclose()
diff --git a/packages/minisky-example/tests/test_example_plugin.py b/packages/minisky-example/tests/test_example_plugin.py
new file mode 100644
index 0000000..a39b2e3
--- /dev/null
+++ b/packages/minisky-example/tests/test_example_plugin.py
@@ -0,0 +1,50 @@
+"""Integration tests owned by the example plugin package."""
+
+from typing import cast
+
+import pytest
+from minisky import Err, MiniSky, MiniSkyConfig
+from minisky_example import Example
+
+
+def run_command(runtime: MiniSky, command: str) -> str:
+ runtime.commands.stack(command)
+ runtime.simulation.step()
+ return runtime.console.read_output_buffer()
+
+
+@pytest.mark.anyio
+async def test_commands_and_entity_are_runtime_owned() -> None:
+ runtime = MiniSky(MiniSkyConfig())
+ try:
+ result = await runtime.plugins.load("EXAMPLE")
+ assert result.is_ok(), result.err()
+ record = runtime.plugins.plugins["EXAMPLE"]
+ assert record.loaded
+ assert tuple(runtime.plugins.loaded_plugins) == ("EXAMPLE",)
+
+ run_command(runtime, "CRE KL001,A320,52,4,90,FL100,250")
+ assert "150" in run_command(runtime, "PASSENGERS KL001 150")
+ assert "150" in run_command(runtime, "PASSENGERS KL001")
+
+ again = await runtime.plugins.load("EXAMPLE")
+ assert again == Err("Plugin EXAMPLE already loaded")
+ finally:
+ await runtime.aclose()
+
+
+@pytest.mark.anyio
+async def test_entity_sizes_existing_traffic_and_retires() -> None:
+ runtime = MiniSky(MiniSkyConfig())
+ runtime.traffic.cre("KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150)
+ result = await runtime.plugins.load("EXAMPLE")
+ assert result.is_ok(), result.err()
+ record = runtime.plugins.plugins["EXAMPLE"]
+ entity = cast(Example, record.entities[0])
+ assert record.entities == (entity,)
+ assert len(entity.npassengers) == 1
+ assert entity._traffic is runtime.traffic
+
+ await runtime.aclose()
+ assert entity._retired
+ assert entity._traffic is None
diff --git a/packages/minisky-tangram/tests/conftest.py b/packages/minisky-tangram/tests/conftest.py
new file mode 100644
index 0000000..807b6b3
--- /dev/null
+++ b/packages/minisky-tangram/tests/conftest.py
@@ -0,0 +1,35 @@
+"""Runtime fixtures for the MiniSky tangram bridge tests."""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterator
+
+import pytest
+from minisky import MiniSky, MiniSkyConfig
+from minisky.simulation import Simulation
+
+
+@pytest.fixture(scope="session")
+def runtime() -> Iterator[MiniSky]:
+ instance = MiniSky(MiniSkyConfig())
+ yield instance
+ instance.close()
+
+
+@pytest.fixture
+def sim(runtime: MiniSky) -> Simulation:
+ runtime.simulation.reset()
+ runtime.console.read_output_buffer()
+ return runtime.simulation
+
+
+@pytest.fixture
+def step_until(runtime: MiniSky) -> Callable[[Callable[[], bool]], int]:
+ def _step(pred: Callable[[], bool], max_steps: int = 600) -> int:
+ for index in range(max_steps):
+ runtime.simulation.step()
+ if pred():
+ return index
+ pytest.fail(f"condition not met within {max_steps} simulation steps")
+
+ return _step
diff --git a/tests/integration/test_tangram_bridge.py b/packages/minisky-tangram/tests/integration/test_tangram_bridge.py
similarity index 100%
rename from tests/integration/test_tangram_bridge.py
rename to packages/minisky-tangram/tests/integration/test_tangram_bridge.py
diff --git a/tests/unit/test_tangram_plugin.py b/packages/minisky-tangram/tests/unit/test_tangram_plugin.py
similarity index 100%
rename from tests/unit/test_tangram_plugin.py
rename to packages/minisky-tangram/tests/unit/test_tangram_plugin.py
diff --git a/tests/_types.py b/packages/minisky/tests/_types.py
similarity index 100%
rename from tests/_types.py
rename to packages/minisky/tests/_types.py
diff --git a/tests/conftest.py b/packages/minisky/tests/conftest.py
similarity index 99%
rename from tests/conftest.py
rename to packages/minisky/tests/conftest.py
index c15212c..7e8a84e 100644
--- a/tests/conftest.py
+++ b/packages/minisky/tests/conftest.py
@@ -13,7 +13,6 @@
from minisky import MiniSky
from minisky.core.config import MiniSkyConfig
from minisky.simulation import Simulation
-
from tests._types import RunCommand, StepUntil
diff --git a/tests/integration/test_conflict.py b/packages/minisky/tests/integration/test_conflict.py
similarity index 99%
rename from tests/integration/test_conflict.py
rename to packages/minisky/tests/integration/test_conflict.py
index ae67e33..efd8372 100644
--- a/tests/integration/test_conflict.py
+++ b/packages/minisky/tests/integration/test_conflict.py
@@ -6,7 +6,6 @@
from minisky import MiniSky
from minisky.simulation import Simulation
from minisky.traffic.asas import MVP
-
from tests._types import RunCommand, StepUntil
FT = 0.3048
diff --git a/tests/integration/test_navdata.py b/packages/minisky/tests/integration/test_navdata.py
similarity index 100%
rename from tests/integration/test_navdata.py
rename to packages/minisky/tests/integration/test_navdata.py
diff --git a/tests/integration/test_plugin.py b/packages/minisky/tests/integration/test_plugin.py
similarity index 86%
rename from tests/integration/test_plugin.py
rename to packages/minisky/tests/integration/test_plugin.py
index 01a6388..b309c88 100644
--- a/tests/integration/test_plugin.py
+++ b/packages/minisky/tests/integration/test_plugin.py
@@ -73,51 +73,8 @@ def install(monkeypatch: pytest.MonkeyPatch, *entries: FakeEntryPoint) -> None:
monkeypatch.setattr(module.metadata, "entry_points", lambda *, group: entries)
-def run_command(runtime: MiniSky, command: str) -> str:
- runtime.commands.stack(command)
- runtime.simulation.step()
- return runtime.console.read_output_buffer()
-
-
@pytest.mark.anyio
-async def test_example_commands_and_entity_are_runtime_owned() -> None:
- runtime = MiniSky(MiniSkyConfig())
- try:
- result = await runtime.plugins.load("EXAMPLE")
- assert result.is_ok(), result.err()
- record = runtime.plugins.plugins["EXAMPLE"]
- assert record.loaded
- assert tuple(runtime.plugins.loaded_plugins) == ("EXAMPLE",)
-
- run_command(runtime, "CRE KL001,A320,52,4,90,FL100,250")
- assert "150" in run_command(runtime, "PASSENGERS KL001 150")
- assert "150" in run_command(runtime, "PASSENGERS KL001")
-
- again = await runtime.plugins.load("EXAMPLE")
- assert again == Err("Plugin EXAMPLE already loaded")
- finally:
- await runtime.aclose()
-
-
@pytest.mark.anyio
-async def test_example_entity_sizes_existing_traffic_and_retires() -> None:
- from minisky_example import Example
-
- runtime = MiniSky(MiniSkyConfig())
- runtime.traffic.cre("KL001", "A320", lat=52.0, lon=4.0, hdg=90, alt=3000, spd=150)
- result = await runtime.plugins.load("EXAMPLE")
- assert result.is_ok(), result.err()
- record = runtime.plugins.plugins["EXAMPLE"]
- entity = cast(Example, record.entities[0])
- assert record.entities == (entity,)
- assert len(entity.npassengers) == 1
- assert entity._traffic is runtime.traffic
-
- await runtime.aclose()
- assert entity._retired
- assert entity._traffic is None
-
-
@pytest.mark.anyio
async def test_entity_backfill_follows_lifespan_startup(
monkeypatch: pytest.MonkeyPatch,
@@ -300,31 +257,6 @@ def build(context: plugin_api.PluginContext[object]) -> plugin_api.PluginSpec:
@pytest.mark.anyio
-async def test_replacement_visibility_is_runtime_local_and_removed_on_shutdown() -> None:
- from minisky_example_customautopilot import CustomAutoPilot
-
- runtime_a = MiniSky(MiniSkyConfig())
- runtime_b = MiniSky(MiniSkyConfig())
- try:
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
- assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
-
- alt_callback = runtime_a.commands.cmddict["ALT"].callback
- result = await runtime_a.plugins.load("CUSTOMAUTOPILOT")
- assert result.is_ok(), result.err()
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_ok()
- assert type(runtime_a.traffic.ap) is CustomAutoPilot
- assert runtime_b.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
-
- await runtime_a.plugins.aclose()
- assert type(runtime_a.traffic.ap) is Autopilot
- assert runtime_a.replaceables.select("AUTOPILOT", "CUSTOMAUTOPILOT").is_err()
- assert runtime_a.commands.cmddict["ALT"].callback is alt_callback
- finally:
- await runtime_a.aclose()
- await runtime_b.aclose()
-
-
@pytest.mark.anyio
async def test_replacement_arrays_size_existing_traffic(
monkeypatch: pytest.MonkeyPatch,
diff --git a/tests/integration/test_route_autopilot.py b/packages/minisky/tests/integration/test_route_autopilot.py
similarity index 99%
rename from tests/integration/test_route_autopilot.py
rename to packages/minisky/tests/integration/test_route_autopilot.py
index 188c5d8..b04067e 100644
--- a/tests/integration/test_route_autopilot.py
+++ b/packages/minisky/tests/integration/test_route_autopilot.py
@@ -10,7 +10,6 @@
from minisky.tools import geo
from minisky.traffic import route as route_commands
from minisky.traffic.route import Route
-
from tests._types import RunCommand, StepUntil
FT = 0.3048
diff --git a/tests/integration/test_scenario.py b/packages/minisky/tests/integration/test_scenario.py
similarity index 99%
rename from tests/integration/test_scenario.py
rename to packages/minisky/tests/integration/test_scenario.py
index a76201d..6303b45 100644
--- a/tests/integration/test_scenario.py
+++ b/packages/minisky/tests/integration/test_scenario.py
@@ -4,7 +4,6 @@
import pytest
from minisky import MiniSky
-
from tests._types import RunCommand, StepUntil
FT = 0.3048
diff --git a/tests/integration/test_stack.py b/packages/minisky/tests/integration/test_stack.py
similarity index 99%
rename from tests/integration/test_stack.py
rename to packages/minisky/tests/integration/test_stack.py
index 1b38fb4..847a3b0 100644
--- a/tests/integration/test_stack.py
+++ b/packages/minisky/tests/integration/test_stack.py
@@ -9,7 +9,6 @@
import pytest
from minisky import MiniSky
from minisky.simulation import Simulation
-
from tests._types import RunCommand
FT = 0.3048
diff --git a/tests/integration/test_streaming.py b/packages/minisky/tests/integration/test_streaming.py
similarity index 99%
rename from tests/integration/test_streaming.py
rename to packages/minisky/tests/integration/test_streaming.py
index 363ce9b..5ae771c 100644
--- a/tests/integration/test_streaming.py
+++ b/packages/minisky/tests/integration/test_streaming.py
@@ -12,7 +12,6 @@
from minisky import MiniSky
from minisky.simulation import Simulation, SimulationState
from minisky.streaming import STREAM_MAX_HZ, StreamHub, build_snapshot
-
from tests._types import RunCommand
diff --git a/tests/integration/test_traffic.py b/packages/minisky/tests/integration/test_traffic.py
similarity index 99%
rename from tests/integration/test_traffic.py
rename to packages/minisky/tests/integration/test_traffic.py
index d32e488..9c96c35 100644
--- a/tests/integration/test_traffic.py
+++ b/packages/minisky/tests/integration/test_traffic.py
@@ -6,7 +6,6 @@
import pytest
from minisky import MiniSky
from minisky.simulation import Simulation
-
from tests._types import RunCommand
FT = 0.3048
diff --git a/tests/test_api.py b/packages/minisky/tests/test_api.py
similarity index 100%
rename from tests/test_api.py
rename to packages/minisky/tests/test_api.py
diff --git a/tests/unit/test_aero.py b/packages/minisky/tests/unit/test_aero.py
similarity index 100%
rename from tests/unit/test_aero.py
rename to packages/minisky/tests/unit/test_aero.py
diff --git a/tests/unit/test_areafilter.py b/packages/minisky/tests/unit/test_areafilter.py
similarity index 100%
rename from tests/unit/test_areafilter.py
rename to packages/minisky/tests/unit/test_areafilter.py
diff --git a/tests/unit/test_convert.py b/packages/minisky/tests/unit/test_convert.py
similarity index 100%
rename from tests/unit/test_convert.py
rename to packages/minisky/tests/unit/test_convert.py
diff --git a/tests/unit/test_detection.py b/packages/minisky/tests/unit/test_detection.py
similarity index 100%
rename from tests/unit/test_detection.py
rename to packages/minisky/tests/unit/test_detection.py
diff --git a/tests/unit/test_geo.py b/packages/minisky/tests/unit/test_geo.py
similarity index 100%
rename from tests/unit/test_geo.py
rename to packages/minisky/tests/unit/test_geo.py
diff --git a/tests/unit/test_phase.py b/packages/minisky/tests/unit/test_phase.py
similarity index 100%
rename from tests/unit/test_phase.py
rename to packages/minisky/tests/unit/test_phase.py
diff --git a/pyproject.toml b/pyproject.toml
index 24844c7..54803a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,7 +12,12 @@ dev = [
docs = ["zensical>=0.0.47", "mkdocstrings-python>=2.0.5"]
[tool.pytest.ini_options]
-testpaths = ["tests"]
+testpaths = [
+ "packages/minisky/tests",
+ "packages/minisky-example/tests",
+ "packages/minisky-example-customautopilot/tests",
+ "packages/minisky-tangram/tests",
+]
pythonpath = [".", "packages/minisky"]
addopts = "-ra -m 'not api'"
markers = ["api: FastAPI endpoint tests, run separately with just test-api"]
@@ -37,7 +42,7 @@ include = [
"packages/minisky-example*/src",
"packages/minisky-tangram/src",
"packages/tangram-minisky/src",
- "tests",
+ "packages/*/tests",
]
exclude = [
"**/.*",
@@ -47,8 +52,8 @@ exclude = [
"build",
"site",
# TODO(abraham): these files will need extensive typing, deferring for now
- "tests/unit/test_aero.py",
- "tests/unit/test_detection.py",
+ "packages/minisky/tests/unit/test_aero.py",
+ "packages/minisky/tests/unit/test_detection.py",
]
extraPaths = [
"packages/minisky",