From ca53e5b8ab5c5379ab60da4d666965035e74d8c4 Mon Sep 17 00:00:00 2001 From: Michael duPont Date: Sun, 26 May 2024 18:30:04 -0400 Subject: [PATCH] Tests linted, typed --- avwx/current/metar.py | 2 +- avwx/current/pirep.py | 2 +- avwx/parsing/speech.py | 8 +- avwx/station/station.py | 2 +- docs/launch.md | 2 +- pyproject.toml | 2 + tests/__init__.py | 4 +- tests/current/test_airsigmet.py | 273 +++++++++--------- tests/current/test_current.py | 20 +- tests/current/test_metar.py | 125 ++++---- tests/current/test_notam.py | 112 ++++---- tests/current/test_pirep.py | 155 +++++----- tests/current/test_taf.py | 242 ++++++++-------- tests/forecast/_test_gfs.py | 16 +- tests/forecast/test_base.py | 96 +++---- tests/forecast/test_nbm.py | 23 +- tests/parsing/cleaners/test_cleaners.py | 31 +- tests/parsing/cleaners/test_cloud.py | 10 +- tests/parsing/cleaners/test_joined.py | 72 +++-- tests/parsing/cleaners/test_remove.py | 16 +- tests/parsing/cleaners/test_replace.py | 16 +- tests/parsing/cleaners/test_separated.py | 172 ++++++----- tests/parsing/cleaners/test_visibility.py | 24 +- tests/parsing/cleaners/test_wind.py | 64 ++--- tests/parsing/test_core.py | 329 +++++++++++----------- tests/parsing/test_remarks.py | 95 +++---- tests/parsing/test_sanitization.py | 24 +- tests/parsing/test_speech.py | 144 +++++----- tests/parsing/test_summary.py | 12 +- tests/parsing/test_translate.py | 163 +++++------ tests/service/test_base.py | 27 +- tests/service/test_bulk.py | 34 +-- tests/service/test_files.py | 35 ++- tests/service/test_scrape.py | 172 ++++++----- tests/test_base.py | 12 +- tests/test_flight_path.py | 43 +-- tests/test_station.py | 206 +++++++------- tests/util.py | 51 ++-- util/build_tests.py | 77 +++-- util/find_bad_stations.py | 54 ++-- util/update_codes.py | 28 +- 41 files changed, 1455 insertions(+), 1540 deletions(-) diff --git a/avwx/current/metar.py b/avwx/current/metar.py index 1205e94..b25b24c 100644 --- a/avwx/current/metar.py +++ b/avwx/current/metar.py @@ -251,7 +251,7 @@ def get_runway_visibility(data: list[str]) -> tuple[list[str], list[RunwayVisibi return data, runway_vis -def parse_altimeter(value: str) -> Number | None: +def parse_altimeter(value: str | None) -> Number | None: """Parse an altimeter string into a Number.""" if not value or len(value) < 4: return None diff --git a/avwx/current/pirep.py b/avwx/current/pirep.py index ab533ba..6d2a0b0 100644 --- a/avwx/current/pirep.py +++ b/avwx/current/pirep.py @@ -157,7 +157,7 @@ def _location(item: str) -> Location | None: return Location(item, station, direction_number, distance_number) -def _time(item: str, target: date | None = None) -> Timestamp | None: +def _time(item: str | None, target: date | None = None) -> Timestamp | None: """Convert a time element to a Timestamp.""" return core.make_timestamp(item, time_only=True, target_date=target) diff --git a/avwx/parsing/speech.py b/avwx/parsing/speech.py index 257b664..9ddeb99 100644 --- a/avwx/parsing/speech.py +++ b/avwx/parsing/speech.py @@ -45,7 +45,7 @@ def wind( return "Winds " + (val or "unknown") -def temperature(header: str, temp: Number, unit: str = "C") -> str: +def temperature(header: str, temp: Number | None, unit: str = "C") -> str: """Format temperature details into a spoken word string.""" if not temp or temp.value is None: return f"{header} unknown" @@ -54,7 +54,7 @@ def temperature(header: str, temp: Number, unit: str = "C") -> str: return " ".join((header, temp.spoken, f"degree{use_s}", unit)) -def visibility(vis: Number, unit: str = "m") -> str: +def visibility(vis: Number | None, unit: str = "m") -> str: """Format visibility details into a spoken word string.""" if not vis: return "Visibility unknown" @@ -78,7 +78,7 @@ def visibility(vis: Number, unit: str = "m") -> str: return ret -def altimeter(alt: Number, unit: str = "inHg") -> str: +def altimeter(alt: Number | None, unit: str = "inHg") -> str: """Format altimeter details into a spoken word string.""" ret = "Altimeter " if not alt: @@ -102,7 +102,7 @@ def wx_codes(codes: list[Code]) -> str: def type_and_times( - type: str, # noqa: A002 + type: str | None, # noqa: A002 start: Timestamp | None, end: Timestamp | None, probability: Number | None = None, diff --git a/avwx/station/station.py b/avwx/station/station.py index 0be718c..c6f74f4 100644 --- a/avwx/station/station.py +++ b/avwx/station/station.py @@ -345,7 +345,7 @@ def nearest( """ # Default state includes all, no filtering necessary if is_airport or sends_reports: - stations = _query_filter(lat, lon, n, max_coord_distance, is_airport, sends_reports) + stations = _query_filter(lat, lon, n, max_coord_distance, is_airport=is_airport, reporting=sends_reports) else: data = _query_coords(lat, lon, n, max_coord_distance) stations = [(Station.from_code(code), d) for code, d in data] diff --git a/docs/launch.md b/docs/launch.md index 6e875b5..4600250 100644 --- a/docs/launch.md +++ b/docs/launch.md @@ -45,7 +45,7 @@ pull request on [GitHub](https://github.com/avwx-rest/avwx-engine) # Installation -AVWX is available on PyPI and requires Python 3.8 and above. Note: the package +AVWX is available on PyPI and requires Python 3.9 and above. Note: the package name is ``avwx-engine``, but the import is ``avwx`` ```bash diff --git a/pyproject.toml b/pyproject.toml index d0d85d4..aa647db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,9 @@ check = "mypy --install-types --non-interactive {args:src/pyorl_example tests}" [tool.hatch.envs.hatch-test] extra-dependencies = [ + "avwx-engine[all]", "pytest-asyncio", + "pytest-cov", "pytest-github-actions-annotate-failures", "time-machine", ] diff --git a/tests/__init__.py b/tests/__init__.py index 5f3ebcc..4e3bfbe 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,4 @@ -""" -AVWX Test Suite -""" +"""AVWX Test Suite.""" # stdlib import sys diff --git a/tests/current/test_airsigmet.py b/tests/current/test_airsigmet.py index 23947f9..012f7b7 100644 --- a/tests/current/test_airsigmet.py +++ b/tests/current/test_airsigmet.py @@ -1,17 +1,18 @@ -""" -AIRMET SIGMET Report Tests -""" +"""AIRMET SIGMET Report Tests.""" + +# ruff: noqa: A002,SLF001 # stdlib +from __future__ import annotations + import json from dataclasses import asdict from datetime import datetime from pathlib import Path -from typing import List, Optional, Tuple # library import pytest -from shapely.geometry import LineString +from shapely.geometry import LineString # type: ignore # module from avwx import structs @@ -23,26 +24,26 @@ # tests from tests.util import assert_code, assert_value, datetime_parser, round_coordinates - # Used for location filtering tests -COORD_REPORTS = [ +_COORD_LOCS = [ "N1000 W01000 - N1000 E01000 - S1000 E01000 - S1000 W01000", "N1000 W00000 - N1000 E02000 - S1000 E02000 - S1000 W00000", "N0000 W00000 - N0000 E02000 - S2000 E02000 - S2000 W00000", "N0000 W00100 - N0000 E01000 - S2000 E01000 - S2000 W01000", ] _PRE = "WAUS43 KKCI 230245 CHIT WA 230245 TS VALID UNTIL 230900 NYC FIR " -COORD_REPORTS = [airsigmet.AirSigmet.from_report(_PRE + r) for r in COORD_REPORTS] +_COORD_REPORTS = [airsigmet.AirSigmet.from_report(_PRE + r) for r in _COORD_LOCS] +COORD_REPORTS: list[airsigmet.AirSigmet] = [i for i in _COORD_REPORTS if i] -def test_repr(): - """Test class name in repr string""" +def test_repr() -> None: + """Test class name in repr string.""" assert repr(airsigmet.AirSigmet()) == "" @pytest.mark.parametrize( - "source,target", - ( + ("source", "target"), + [ ("1...2..3.. 4. 5.1 6.", "1 2 3 4 5.1 6"), ("CIG BLW 010/VIS PCPN/BR/FG.", "CIG BLW 010 VIS PCPN/BR/FG"), ( @@ -50,22 +51,22 @@ def test_repr(): "THRU 15Z FRZLVL RANGING FROM", ), ("AIRMET ICE...NV UT", "AIRMET ICE NV UT"), - ), + ], ) -def test_parse_prep(source: str, target: str): - """Test report elements are replaced to aid parsing""" +def test_parse_prep(source: str, target: str) -> None: + """Test report elements are replaced to aid parsing.""" assert airsigmet._parse_prep(source) == target.split() @pytest.mark.parametrize( - "repr,type,country,number", - ( + ("repr", "type", "country", "number"), + [ ("WSRH31", "sigmet", "RH", 31), ("WAUS01", "airmet", "US", 1), - ), + ], ) -def test_bulletin(repr: str, type: str, country: str, number: int): - """Test Bulletin parsing""" +def test_bulletin(repr: str, type: str, country: str, number: int) -> None: + """Test Bulletin parsing.""" bulletin = airsigmet._bulletin(repr) assert bulletin.repr == repr assert bulletin.type.value == type @@ -74,16 +75,14 @@ def test_bulletin(repr: str, type: str, country: str, number: int): @pytest.mark.parametrize( - "wx,bulletin,issuer,time,correction", - ( + ("wx", "bulletin", "issuer", "time", "correction"), + [ ("WSCI35 ZGGG 210031 1 2", "WSCI35", "ZGGG", "210031", None), ("WAUS46 KKCI 230416 AAA 1 2", "WAUS46", "KKCI", "230416", "AAA"), - ), + ], ) -def test_header( - wx: str, bulletin: str, issuer: str, time: str, correction: Optional[str] -): - """Test report header element extraction""" +def test_header(wx: str, bulletin: str, issuer: str, time: str, correction: str | None) -> None: + """Test report header element extraction.""" ret_wx, *ret = airsigmet._header(wx.split()) assert ret_wx == ["1", "2"] assert ret[0] == airsigmet._bulletin(bulletin) @@ -93,8 +92,8 @@ def test_header( @pytest.mark.parametrize( - "wx,area,report_type,start_time,end_time,station", - ( + ("wx", "area", "report_type", "start_time", "end_time", "station"), + [ ( "SIGE CONVECTIVE SIGMET 74E VALID UNTIL 2255Z 1 2", "SIGE", @@ -127,17 +126,17 @@ def test_header( "251100", "SBAZ", ), - ), + ], ) def test_spacetime( wx: str, area: str, report_type: str, - start_time: Optional[str], + start_time: str | None, end_time: str, - station: Optional[str], -): - """Text place, type, and timestamp extraction""" + station: str | None, +) -> None: + """Text place, type, and timestamp extraction.""" ret_wx, *ret = airsigmet._spacetime(wx.split()) assert ret_wx == ["1", "2"] assert ret[0] == area @@ -148,38 +147,38 @@ def test_spacetime( @pytest.mark.parametrize( - "index,targets", - ( + ("index", "targets"), + [ (1, ("1",)), (4, ("4", "8")), (2, ("a", "b", "2", "0")), - ), + ], ) -def test_first_index(index: int, targets: Tuple[str]): - """Test util to find the first occurence of n strings""" +def test_first_index(index: int, targets: tuple[str]) -> None: + """Test util to find the first occurence of n strings.""" source = [str(i) for i in range(10)] assert airsigmet._first_index(source, *targets) == index @pytest.mark.parametrize( - "wx,region,extra", - ( + ("wx", "region", "extra"), + [ ("FL CSTL WTRS FROM 100SSW", "FL CSTL WTRS", "FROM 100SSW"), ("OH LE 50W", "OH LE", "50W"), ("OH LE FROM 50W", "OH LE", "FROM 50W"), ("ZGZU GUANGZHOU FIR SEV ICE", "ZGZU GUANGZHOU FIR", "SEV ICE"), - ), + ], ) -def test_region(wx: str, region: str, extra: str): - """Test region extraction""" +def test_region(wx: str, region: str, extra: str) -> None: + """Test region extraction.""" ret_wx, ret_region = airsigmet._region(wx.split()) assert ret_wx == extra.split() assert ret_region == region @pytest.mark.parametrize( - "wx,start,end,extra", - ( + ("wx", "start", "end", "extra"), + [ ( "OUTLOOK VALID 230555-230955 FROM 30SSW", core.make_timestamp("230555"), @@ -188,22 +187,20 @@ def test_region(wx: str, region: str, extra: str): ), ( "THRU 15Z OTLK VALID 0900-1500Z", - core.make_timestamp("0900", True), - core.make_timestamp("1500Z", True), + core.make_timestamp("0900", time_only=True), + core.make_timestamp("1500Z", time_only=True), "THRU 15Z ", ), ( "VA CLD OBS AT 0110Z SFC/FL160", - core.make_timestamp("0110Z", True), + core.make_timestamp("0110Z", time_only=True), None, "VA CLD SFC/FL160", ), - ), + ], ) -def test_time( - wx: str, start: structs.Timestamp, end: Optional[structs.Timestamp], extra: str -): - """Test observation start and end time extraction""" +def test_time(wx: str, start: structs.Timestamp, end: structs.Timestamp | None, extra: str) -> None: + """Test observation start and end time extraction.""" ret_wx, ret_start, ret_end = airsigmet._time(wx.split()) assert ret_wx == extra.split() assert ret_start == start @@ -211,42 +208,42 @@ def test_time( @pytest.mark.parametrize( - "coord,value", - ( + ("coord", "value"), + [ ("N1429", 14.29), ("S0250", -2.5), ("N2900", 29.0), ("W09053", -90.53), ("E01506", 15.06), ("E15000", 150.0), - ), + ], ) -def test_coord_value(coord: str, value: float): - """Test string to float coordinate component""" +def test_coord_value(coord: str, value: float) -> None: + """Test string to float coordinate component.""" assert airsigmet._coord_value(coord) == value @pytest.mark.parametrize( - "wx,coord,extra", - ( + ("wx", "coord", "extra"), + [ ("1 2 3", None, "1 2 3"), ( "VA FUEGO PSN N1428 W09052 VA", Coord(14.28, -90.52, "N1428 W09052"), "VA FUEGO VA", ), - ), + ], ) -def test_position(wx: str, coord: Optional[Coord], extra: str): - """Test position coordinate extraction""" +def test_position(wx: str, coord: Coord | None, extra: str) -> None: + """Test position coordinate extraction.""" ret_wx, ret_coord = airsigmet._position(wx.split()) assert ret_wx == extra.split() assert ret_coord == coord @pytest.mark.parametrize( - "wx,movement,unit,extra", - ( + ("wx", "movement", "unit", "extra"), + [ ("1 2 3", None, "kt", "1 2 3"), ("270226Z CNL MOV CNL", None, "kt", "270226Z CNL"), ( @@ -267,9 +264,7 @@ def test_position(wx: str, coord: Optional[Coord], extra: str): ), ( "AREA TS MOV FROM 23040KT", - Movement( - "MOV FROM 23040KT", core.make_number("230"), core.make_number("40") - ), + Movement("MOV FROM 23040KT", core.make_number("230"), core.make_number("40")), "kt", "AREA TS", ), @@ -303,10 +298,10 @@ def test_position(wx: str, coord: Optional[Coord], extra: str): "kt", "TOP FL390", ), - ), + ], ) -def test_movement(wx: str, movement: Optional[Movement], unit: str, extra: str): - """Test weather movement extraction""" +def test_movement(wx: str, movement: Movement | None, unit: str, extra: str) -> None: + """Test weather movement extraction.""" units = Units.international() ret_wx, units, ret_movement = airsigmet._movement(wx.split(), units) assert ret_wx == extra.split() @@ -315,8 +310,8 @@ def test_movement(wx: str, movement: Optional[Movement], unit: str, extra: str): @pytest.mark.parametrize( - "report,bounds,start,extra", - ( + ("report", "bounds", "start", "extra"), + [ ( "FCST N OF N2050 AND S OF N2900 FL060/300", ["N OF N2050", "S OF N2900"], @@ -324,10 +319,10 @@ def test_movement(wx: str, movement: Optional[Movement], unit: str, extra: str): "FCST AND FL060/300", ), ("FCST N OF N33 TOP", ["N OF N33"], 5, "FCST TOP"), - ), + ], ) -def test_bounds_from_latterals(report: str, bounds: List[str], start: int, extra: str): - """Test extracting latteral bound strings from the report""" +def test_bounds_from_latterals(report: str, bounds: list[str], start: int, extra: str) -> None: + """Test extracting latteral bound strings from the report.""" ret_report, ret_bounds, ret_start = airsigmet._bounds_from_latterals(report, -1) assert ret_report == extra assert ret_bounds == bounds @@ -335,8 +330,8 @@ def test_bounds_from_latterals(report: str, bounds: List[str], start: int, extra @pytest.mark.parametrize( - "report,coords,start,extra", - ( + ("report", "coords", "start", "extra"), + [ ( "LINE BTN N6916 W06724 - N6825 W06700 - N6753 W06524", ((69.16, -67.24), (68.25, -67.00), (67.53, -65.24)), @@ -349,10 +344,10 @@ def test_bounds_from_latterals(report: str, bounds: List[str], start: int, extra 3, "WI N6753 W06524", ), - ), + ], ) -def test_coords_from_text(report: str, coords: Tuple[tuple], start: int, extra: str): - """Test extracting coords from NESW string pairs in the report""" +def test_coords_from_text(report: str, coords: tuple[tuple], start: int, extra: str) -> None: + """Test extracting coords from NESW string pairs in the report.""" ret_report, ret_coords, ret_start = airsigmet._coords_from_text(report, -1) assert ret_report == extra assert ret_start == start @@ -362,8 +357,8 @@ def test_coords_from_text(report: str, coords: Tuple[tuple], start: int, extra: @pytest.mark.parametrize( - "report,coords,start,extra", - ( + ("report", "coords", "start", "extra"), + [ ( "1 FROM 30SSW BNA-40W MGM-30ENE LSU 1", ((35.67, -86.92), (32.22, -87.11), (4.74, 115.96)), @@ -376,10 +371,10 @@ def test_coords_from_text(report: str, coords: Tuple[tuple], start: int, extra: 5, "FROM ", ), - ), + ], ) -def test_coords_from_navaids(report: str, coords: Tuple[tuple], start: int, extra: str): - """Test extracting coords from relative navaidlocations in the report""" +def test_coords_from_navaids(report: str, coords: tuple[tuple], start: int, extra: str) -> None: + """Test extracting coords from relative navaidlocations in the report.""" ret_report, ret_coords, ret_start = airsigmet._coords_from_navaids(report, -1) assert ret_report == extra assert ret_start == start @@ -389,8 +384,8 @@ def test_coords_from_navaids(report: str, coords: Tuple[tuple], start: int, extr @pytest.mark.parametrize( - "wx,coords,bounds,extra", - ( + ("wx", "coords", "bounds", "extra"), + [ ( "FCST N OF N2050 AND S OF N2900 FL060/300", [], @@ -421,10 +416,10 @@ def test_coords_from_navaids(report: str, coords: Tuple[tuple], start: int, extr [], "", ), - ), + ], ) -def test_bounds(wx: str, coords: Tuple[tuple], bounds: List[str], extra: str): - """Test extracting all pre-break bounds and coordinates from the report""" +def test_bounds(wx: str, coords: tuple[tuple], bounds: list[str], extra: str) -> None: + """Test extracting all pre-break bounds and coordinates from the report.""" ret_wx, ret_coords, ret_bounds = airsigmet._bounds(wx.split()) assert ret_wx == extra.split() assert ret_bounds == bounds @@ -434,8 +429,8 @@ def test_bounds(wx: str, coords: Tuple[tuple], bounds: List[str], extra: str): @pytest.mark.parametrize( - "wx,floor,ceiling,unit,extra", - ( + ("wx", "floor", "ceiling", "unit", "extra"), + [ ("1 FL060/300 2", 60, 300, "ft", "1 2"), ("0110Z SFC/FL160", 0, 160, "ft", "0110Z"), ("0110Z SFC/10000FT", 0, 10000, "ft", "0110Z"), @@ -445,12 +440,10 @@ def test_bounds(wx: str, coords: Tuple[tuple], bounds: List[str], extra: str): ("TURB BTN FL180 AND FL330", 180, 330, "ft", "TURB"), ("1 CIG BLW 010 2", None, 10, "ft", "1 2"), ("1 TOP BLW FL380 2", None, 380, "ft", "1 2"), - ), + ], ) -def test_altitudes( - wx: str, floor: Optional[int], ceiling: Optional[int], unit: str, extra: str -): - """Tests extracting floor and ceiling altitudes from report""" +def test_altitudes(wx: str, floor: int | None, ceiling: int | None, unit: str, extra: str) -> None: + """Test extracting floor and ceiling altitudes from report.""" units = Units.international() ret_wx, units, ret_floor, ret_ceiling = airsigmet._altitudes(wx.split(), units) assert ret_wx == extra.split() @@ -460,52 +453,52 @@ def test_altitudes( @pytest.mark.parametrize( - "wx,code,name,extra", - ( + ("wx", "code", "name", "extra"), + [ ("1 2 3 4", None, None, "1 2 3 4"), ("FIR SEV ICE FCST N", "SEV ICE", "Severe icing", "FIR FCST N"), ("W09052 VA CLD OBS AT", "VA CLD", "Volcanic cloud", "W09052 OBS AT"), - ), + ], ) -def test_weather_type(wx: str, code: Optional[str], name: Optional[str], extra: str): - """Tests extracting weather type code from report""" +def test_weather_type(wx: str, code: str | None, name: str | None, extra: str) -> None: + """Test extracting weather type code from report.""" ret_wx, weather = airsigmet._weather_type(wx.split()) assert ret_wx == extra.split() assert_code(weather, code, name) @pytest.mark.parametrize( - "wx,code,name,extra", - ( + ("wx", "code", "name", "extra"), + [ ("1 2 3 4", None, None, "1 2 3 4"), ("1 2 3 NC", "NC", "No change", "1 2 3"), ("1 2 3 INTSF", "INTSF", "Intensifying", "1 2 3"), ("1 2 3 WKN", "WKN", "Weakening", "1 2 3"), - ), + ], ) -def test_intensity(wx: str, code: Optional[str], name: Optional[str], extra: str): - """Tests extracting intensity code from report""" +def test_intensity(wx: str, code: str | None, name: str | None, extra: str) -> None: + """Test extracting intensity code from report.""" ret_wx, intensity = airsigmet._intensity(wx.split()) assert ret_wx == extra.split() assert_code(intensity, code, name) @pytest.mark.parametrize( - "report,clean", - ( + ("report", "clean"), + [ ("WAUS43 KKCI 1 \n 2 3 NC=", "WAUS43 KKCI 1 2 3 NC"), ("TOP FL520 MO V NNW 05KT NC", "TOP FL520 MOV NNW 05KT NC"), ("FL450 MOV NE05KT INTSF=", "FL450 MOV NE 05KT INTSF"), ("TOP FL420 MOV E2KT INTSF=", "TOP FL420 MOV E 2KT INTSF"), - ), + ], ) -def test_sanitize(report: str, clean: str): - """Tests report sanitization""" +def test_sanitize(report: str, clean: str) -> None: + """Test report sanitization.""" assert airsigmet.sanitize(report) == clean -def test_parse(): - """Tests returned structs from the parse function""" +def test_parse() -> None: + """Test returned structs from the parse function.""" report = ( "WAUS43 KKCI 230245 CHIT WA 230245 AIRMET TANGO FOR TURB AND LLWS " "VALID UNTIL 230900 AIRMET TURB...ND SD NE MN IA WI LM LS MI LH " @@ -520,33 +513,33 @@ def test_parse(): @pytest.mark.parametrize( - "lat,lon,results", - ( + ("lat", "lon", "results"), + [ (5, 5, (True, True, False, False)), (5, -15, (False, False, False, False)), (5, 15, (False, True, False, False)), (5, -5, (True, False, False, False)), (0, 0, (True, False, False, False)), - ), + ], ) -def test_contains(lat: int, lon: int, results: tuple): - """Tests if report contains a coordinate""" +def test_contains(lat: int, lon: int, results: tuple) -> None: + """Test if report contains a coordinate.""" coord = Coord(lat, lon) for report, result in zip(COORD_REPORTS, results): assert report.contains(coord) == result @pytest.mark.parametrize( - "coords,results", - ( + ("coords", "results"), + [ (((20, 20), (0, 0), (-20, -20)), (True, True, True, True)), (((20, 20), (10, 0), (-20, -20)), (True, True, False, False)), (((-20, 20), (-10, -10), (-20, -20)), (True, False, True, True)), (((-20, 20), (-15, -15), (-20, -20)), (False, False, True, True)), - ), + ], ) -def test_intersects(coords: tuple, results: tuple): - """Testsif report intersects a path""" +def test_intersects(coords: tuple, results: tuple) -> None: + """Test if report intersects a path.""" path = LineString(coords) for report, result in zip(COORD_REPORTS, results): assert report.intersects(path) == result @@ -555,11 +548,9 @@ def test_intersects(coords: tuple, results: tuple): AIRSIG_PATH = Path(__file__).parent / "data" / "airsigmet.json" -@pytest.mark.parametrize( - "ref", json.load(AIRSIG_PATH.open(), object_hook=datetime_parser) -) -def test_airsigmet_ete(ref: dict): - """Performs an end-to-end test of reports in the AIRSIGMET JSON file""" +@pytest.mark.parametrize("ref", json.load(AIRSIG_PATH.open(), object_hook=datetime_parser)) +def test_airsigmet_ete(ref: dict) -> None: + """Perform an end-to-end test of reports in the AIRSIGMET JSON file.""" created = ref.pop("created").date() airsig = airsigmet.AirSigmet() assert airsig.last_updated is None @@ -574,17 +565,17 @@ def test_airsigmet_ete(ref: dict): @pytest.mark.parametrize( - "lat,lon,count", - ( + ("lat", "lon", "count"), + [ (5, 5, 2), (5, -15, 0), (5, 15, 1), (5, -5, 1), (0, 0, 1), - ), + ], ) -def test_manager_contains(lat: int, lon: int, count: int): - """Tests filtering reports that contain a coordinate""" +def test_manager_contains(lat: int, lon: int, count: int) -> None: + """Test filtering reports that contain a coordinate.""" manager = airsigmet.AirSigManager() manager.reports = COORD_REPORTS coord = Coord(lat, lon) @@ -592,16 +583,16 @@ def test_manager_contains(lat: int, lon: int, count: int): @pytest.mark.parametrize( - "coords,count", - ( + ("coords", "count"), + [ (((20, 20), (0, 0), (-20, -20)), 4), (((20, 20), (10, 0), (-20, -20)), 2), (((-20, 20), (-10, -10), (-20, -20)), 3), (((-20, 20), (-15, -15), (-20, -20)), 2), - ), + ], ) -def test_along(coords: tuple, count: int): - """Tests filtering reports the fall along a flight path""" +def test_along(coords: tuple, count: int) -> None: + """Test filtering reports the fall along a flight path.""" manager = airsigmet.AirSigManager() manager.reports = COORD_REPORTS path = [Coord(c[0], c[1]) for c in coords] diff --git a/tests/current/test_current.py b/tests/current/test_current.py index 3140771..9ab475b 100644 --- a/tests/current/test_current.py +++ b/tests/current/test_current.py @@ -1,6 +1,4 @@ -""" -Current Report Base Tests -""" +"""Current Report Base Tests.""" # library import pytest @@ -11,24 +9,24 @@ @pytest.mark.parametrize( - "code,value", - ( + ("code", "value"), + [ ("+RATS", "Heavy Rain Thunderstorm"), ("VCFC", "Vicinity Funnel Cloud"), ("-GR", "Light Hail"), ("FZFG", "Freezing Fog"), ("BCBLSN", "Patchy Blowing Snow"), - ), + ], ) -def test_wxcode(code: str, value: str): - """Tests expanding weather codes or ignoring them""" +def test_wxcode(code: str, value: str) -> None: + """Test expanding weather codes or ignoring them.""" obj = Code(code, value) assert current.base.wx_code(code) == obj @pytest.mark.parametrize( - "code,value", - (("", ""), ("R03/03002V03", "R03/03002V03"), ("CB", "CB"), ("0800SE", "0800SE")), + ("code", "value"), + [("", ""), ("R03/03002V03", "R03/03002V03"), ("CB", "CB"), ("0800SE", "0800SE")], ) -def test_unknown_code(code: str, value: str): +def test_unknown_code(code: str, value: str) -> None: assert current.base.wx_code(code) == value diff --git a/tests/current/test_metar.py b/tests/current/test_metar.py index 4698874..8d66f86 100644 --- a/tests/current/test_metar.py +++ b/tests/current/test_metar.py @@ -1,13 +1,10 @@ -""" -METAR Report Tests -""" - -# pylint: disable=invalid-name +"""METAR Report Tests.""" # stdlib +from __future__ import annotations + from dataclasses import asdict from datetime import datetime -from typing import List, Optional, Tuple # library import pytest @@ -20,30 +17,30 @@ from tests.util import assert_number, get_data -def test_repr(): - """Test type and code in repr string""" +def test_repr() -> None: + """Test type and code in repr string.""" assert repr(metar.Metar("KMCO")) == "" @pytest.mark.parametrize( - "raw,wx,rmk", - ( + ("raw", "wx", "rmk"), + [ ("1 2 3 A2992 RMK Hi", ["1", "2", "3", "A2992"], "RMK Hi"), ("1 2 3 A2992 Hi", ["1", "2", "3", "A2992"], "Hi"), ("1 2 Q0900 NOSIG", ["1", "2", "Q0900"], "NOSIG"), ("1 2 3 BLU+ Hello", ["1", "2", "3"], "BLU+ Hello"), - ), + ], ) -def test_get_remarks(raw: str, wx: List[str], rmk: str): - """Remarks get removed first with the remaining components split into a list""" +def test_get_remarks(raw: str, wx: list[str], rmk: str) -> None: + """Remarks get removed first with the remaining components split into a list.""" test_wx, test_rmk = metar.get_remarks(raw) assert wx == test_wx assert rmk == test_rmk @pytest.mark.parametrize( - "wx,temp,dew", - ( + ("wx", "temp", "dew"), + [ (["1", "2"], (None,), (None,)), (["1", "2", "07/05"], ("07", 7), ("05", 5)), (["07/05", "1", "2"], ("07", 7), ("05", 5)), @@ -52,23 +49,23 @@ def test_get_remarks(raw: str, wx: List[str], rmk: str): (["10///", "1", "2"], ("10", 10), (None,)), (["/////", "1", "2"], (None,), (None,)), (["XX/01", "1", "2"], (None,), ("01", 1)), - ), + ], ) -def test_get_temp_and_dew(wx: List[str], temp: tuple, dew: tuple): - """Tests temperature and dewpoint extraction""" +def test_get_temp_and_dew(wx: list[str], temp: tuple, dew: tuple) -> None: + """Test temperature and dewpoint extraction.""" ret_wx, ret_temp, ret_dew = metar.get_temp_and_dew(wx) assert ret_wx == ["1", "2"] assert_number(ret_temp, *temp) assert_number(ret_dew, *dew) -def test_not_temp_or_dew(): +def test_not_temp_or_dew() -> None: assert metar.get_temp_and_dew(["MX/01"]) == (["MX/01"], None, None) @pytest.mark.parametrize( - "temp,dew,rmk,humidity", - ( + ("temp", "dew", "rmk", "humidity"), + [ (None, None, "", None), (12, 5, "", 0.62228), (12, 5, "RMK T01230054", 0.62732), @@ -76,27 +73,23 @@ def test_not_temp_or_dew(): (None, 12, "", None), (12, None, "", None), (12, None, "RMK T12341345", 0.35408), - ), + ], ) -def test_get_relative_humidity( - temp: Optional[int], dew: Optional[int], rmk: str, humidity: Optional[float] -): - """Tests calculating relative humidity from available temperatures""" +def test_get_relative_humidity(temp: int | None, dew: int | None, rmk: str, humidity: float | None) -> None: + """Test calculating relative humidity from available temperatures.""" units = metar.Units.north_american() - if temp is not None: - temp = metar.Number("", temp, "") - if dew is not None: - dew = metar.Number("", dew, "") + temp_num = structs.Number("", temp, "") if temp is not None else None + dew_num = structs.Number("", dew, "") if dew is not None else None remarks_info = metar.remarks.parse(rmk) - value = metar.get_relative_humidity(temp, dew, remarks_info, units) + value = metar.get_relative_humidity(temp_num, dew_num, remarks_info, units) if value is not None: value = round(value, 5) assert humidity == value @pytest.mark.parametrize( - "text,alt", - ( + ("text", "alt"), + [ ("A2992", (29.92, "two nine point nine two")), ("2992", (29.92, "two nine point nine two")), ("A3000", (30.00, "three zero point zero zero")), @@ -105,21 +98,21 @@ def test_get_relative_humidity( ("Q0998", (998, "zero nine nine eight")), ("Q1000/10", (1000, "one zero zero zero")), ("QNH3003INS", (30.03, "three zero point zero three")), - ), + ], ) -def test_parse_altimeter(text: str, alt: Tuple[float, str]): - """Tests that an atlimiter is correctly parsed into a Number""" +def test_parse_altimeter(text: str, alt: tuple[float, str]) -> None: + """Test that an atlimiter is correctly parsed into a Number.""" assert_number(metar.parse_altimeter(text), text, *alt) -@pytest.mark.parametrize("text", (None, "12/10", "RMK", "ABCDE", "15KM", "10SM")) -def test_bad_altimeter(text: Optional[str]): +@pytest.mark.parametrize("text", [None, "12/10", "RMK", "ABCDE", "15KM", "10SM"]) +def test_bad_altimeter(text: str | None) -> None: assert metar.parse_altimeter(text) is None @pytest.mark.parametrize( - "version,wx,alt,unit", - ( + ("version", "wx", "alt", "unit"), + [ ("NA", ["1"], (None,), "inHg"), ("NA", ["1", "A2992"], ("A2992", 29.92, "two nine point nine two"), "inHg"), ( @@ -164,10 +157,10 @@ def test_bad_altimeter(text: Optional[str]): ("QNH3003INS", 30.03, "three zero point zero three"), "inHg", ), - ), + ], ) -def test_get_altimeter(version: str, wx: List[str], alt: tuple, unit: str): - """Tests that the correct alimeter item gets removed from the end of the wx list""" +def test_get_altimeter(version: str, wx: list[str], alt: tuple, unit: str) -> None: + """Test that the correct alimeter item gets removed from the end of the wx list.""" units = structs.Units(**getattr(static.core, f"{version}_UNITS")) ret, ret_alt = metar.get_altimeter(wx, units, version) assert ret == ["1"] @@ -176,8 +169,8 @@ def test_get_altimeter(version: str, wx: List[str], alt: tuple, unit: str): @pytest.mark.parametrize( - "value,runway,vis,var,trend", - ( + ("value", "runway", "vis", "var", "trend"), + [ ("R35L/1000", "35L", ("1000", 1000, "one thousand"), None, None), ("R06/M0500", "06", ("M0500", None, "less than five hundred"), None, None), ("R33/////", "33", None, None, None), @@ -219,16 +212,16 @@ def test_get_altimeter(version: str, wx: List[str], alt: tuple, unit: str): None, structs.Code("N", "no change"), ), - ), + ], ) def test_parse_runway_visibility( value: str, runway: str, vis: tuple, - var: Optional[tuple], - trend: Optional[structs.Code], -): - """Tests parsing runway visibility range values""" + var: tuple | None, + trend: structs.Code | None, +) -> None: + """Test parsing runway visibility range values.""" rvr = metar.parse_runway_visibility(value) assert rvr.runway == runway if vis is None: @@ -242,29 +235,27 @@ def test_parse_runway_visibility( @pytest.mark.parametrize( - "wx,count", - ( + ("wx", "count"), + [ (["1", "2"], 0), (["1", "2", "R10/10"], 1), (["1", "2", "R02/05", "R34/04"], 2), - ), + ], ) -def test_get_runway_visibility(wx: List[str], count: int): - """Tests extracting runway visibility""" +def test_get_runway_visibility(wx: list[str], count: int) -> None: + """Test extracting runway visibility.""" items, rvr = metar.get_runway_visibility(wx) assert items == ["1", "2"] assert len(rvr) == count -def test_sanitize(): - """Tests report sanitization""" +def test_sanitize() -> None: + """Test report sanitization.""" report = "METAR AUTO KJFK 032151ZVRB08KT FEW034BKN250 ? C A V O K RMK TEST" clean = "KJFK 032151Z VRB08KT FEW034 BKN250 CAVOK RMK TEST" remarks = "RMK TEST" data = ["KJFK", "032151Z", "VRB08KT", "FEW034", "BKN250", "CAVOK"] - sans = structs.Sanitization( - ["METAR", "AUTO", "?"], {"C A V O K": "CAVOK"}, extra_spaces_needed=True - ) + sans = structs.Sanitization(["METAR", "AUTO", "?"], {"C A V O K": "CAVOK"}, extra_spaces_needed=True) ret_clean, ret_remarks, ret_data, ret_sans = metar.sanitize(report) assert clean == ret_clean assert remarks == ret_remarks @@ -272,8 +263,8 @@ def test_sanitize(): assert sans == ret_sans -def test_parse(): - """Tests returned structs from the parse function""" +def test_parse() -> None: + """Test returned structs from the parse function.""" report = "KJFK 032151Z 16008KT 10SM FEW034 FEW130 BKN250 27/23 A3013 RMK AO2 SLP201" data, units, sans = metar.parse(report[:4], report) assert isinstance(data, structs.MetarData) @@ -282,8 +273,8 @@ def test_parse(): assert data.raw == report -def test_parse_awos(): - """Tests an AWOS weather report. Only used for advisory""" +def test_parse_awos() -> None: + """Test an AWOS weather report. Only used for advisory.""" report = "3J0 140347Z AUTO 05003KT 07/02 RMK ADVISORY A01 $" data, units, sans = metar.parse("KJFK", report, use_na=True) assert isinstance(data, structs.MetarData) @@ -293,9 +284,9 @@ def test_parse_awos(): assert units.altimeter == "inHg" -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "metar")) -def test_metar_ete(ref: dict, icao: str, issued: datetime): - """Performs an end-to-end test of all METAR JSON files""" +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "metar")) +def test_metar_ete(ref: dict, icao: str, issued: datetime) -> None: + """Perform an end-to-end test of all METAR JSON files.""" station = metar.Metar(icao) raw = ref["data"]["raw"] assert station.sanitize(raw) == ref["data"]["sanitized"] diff --git a/tests/current/test_notam.py b/tests/current/test_notam.py index 5a00485..0057d82 100644 --- a/tests/current/test_notam.py +++ b/tests/current/test_notam.py @@ -1,11 +1,13 @@ -""" -NOTAM Report Tests -""" +"""NOTAM Report Tests.""" + +# ruff: noqa: SLF001 # stdlib +from __future__ import annotations + from dataclasses import asdict from datetime import datetime, timezone -from typing import Optional +from typing import Any # library import pytest @@ -18,7 +20,6 @@ # tests from tests.util import get_data - QUALIFIERS = [ structs.Qualifiers( repr="ZNY/QPIXX/I/NBO/A/000/999/4038N07346W025", @@ -169,29 +170,32 @@ @pytest.mark.parametrize( - "text,lat,lon", - ( + ("text", "lat", "lon"), + [ ("5126N00036W", 51.26, -0.36), ("1234S14321E", -12.34, 143.21), ("2413N01234W", 24.13, -12.34), - ), + ], ) -def test_rear_coord(text: str, lat: float, lon: float): - """Tests converting rear-loc coord string to Coord struct""" +def test_rear_coord(text: str, lat: float, lon: float) -> None: + """Test converting rear-loc coord string to Coord struct.""" coord = structs.Coord(lat, lon, text) assert notam._rear_coord(text) == coord -@pytest.mark.parametrize("text", ("latNlongE", "2102N086")) -def test_bad_rear_coord(text: str): +@pytest.mark.parametrize("text", ["latNlongE", "2102N086"]) +def test_bad_rear_coord(text: str) -> None: assert notam._rear_coord(text) is None @pytest.mark.parametrize( - "text,number,char,rtype,replaces", (("01/113 NOTAMN", "01/113", "N", "New", None),) + ("text", "number", "char", "rtype", "replaces"), + [ + ("01/113 NOTAMN", "01/113", "N", "New", None), + ], ) -def test_header(text: str, number: str, char: str, rtype: str, replaces: Optional[str]): - """Tests parsing NOTAM headers""" +def test_header(text: str, number: str, char: str, rtype: str, replaces: str | None) -> None: + """Test parsing NOTAM headers.""" ret_number, ret_type, ret_replaces = notam._header(text) code = structs.Code(f"NOTAM{char}", rtype) assert ret_number == number @@ -200,15 +204,15 @@ def test_header(text: str, number: str, char: str, rtype: str, replaces: Optiona @pytest.mark.parametrize("qualifier", QUALIFIERS) -def test_qualifiers(qualifier: structs.Qualifiers): - """Tests Qualifier struct parsing""" +def test_qualifiers(qualifier: structs.Qualifiers) -> None: + """Test Qualifier struct parsing.""" units = structs.Units.international() assert notam._qualifiers(qualifier.repr, units) == qualifier @pytest.mark.parametrize( - "raw,trim,tz,dt", - ( + ("raw", "trim", "tz", "dt"), + [ ( "2110121506", "2110121506", @@ -221,34 +225,34 @@ def test_qualifiers(qualifier: structs.Qualifiers): "EST", datetime(2022, 5, 24, 14, 52, tzinfo=gettz("EST")), ), - ), + ], ) -def test_make_year_timestamp(raw: str, trim: str, tz: Optional[str], dt: datetime): - """Tests datetime conversion from year-prefixed strings""" +def test_make_year_timestamp(raw: str, trim: str, tz: str | None, dt: datetime) -> None: + """Test datetime conversion from year-prefixed strings.""" timestamp = structs.Timestamp(raw, dt) assert notam.make_year_timestamp(trim, raw, tz) == timestamp @pytest.mark.parametrize( - "raw,code,value", - ( + ("raw", "code", "value"), + [ ("PERM", "PERM", "Permanent"), ("PERM\n123", "PERM", "Permanent"), ("WIE", "WIE", "With Immediate Effect"), - ), + ], ) -def test_timestamp_codes(raw: str, code: str, value: str): - """Tests datetime conversion when given a known code""" +def test_timestamp_codes(raw: str, code: str, value: str) -> None: + """Test datetime conversion when given a known code.""" assert notam.make_year_timestamp(raw, raw) == structs.Code(code, value) -def test_bad_year_timestamp(): +def test_bad_year_timestamp() -> None: assert notam.make_year_timestamp(" ", " ", None) is None @pytest.mark.parametrize( - "start,end,start_dt,end_dt", - ( + ("start", "end", "start_dt", "end_dt"), + [ ( "2107221958", "2307221957EST", @@ -280,12 +284,10 @@ def test_bad_year_timestamp(): datetime(2023, 6, 4, 22, 0, tzinfo=gettz("EST")), ), ("", "", None, None), - ), + ], ) -def test_parse_linked_times( - start: str, end: str, start_dt: Optional[datetime], end_dt: Optional[datetime] -): - """Tests parsing start and end times with shared timezone""" +def test_parse_linked_times(start: str, end: str, start_dt: datetime | None, end_dt: datetime | None) -> None: + """Test parsing start and end times with shared timezone.""" ret_start, ret_end = notam.parse_linked_times(start, end) start_comp = structs.Timestamp(start, start_dt) if start_dt else None end_comp = structs.Timestamp(end, end_dt) if end_dt else None @@ -294,32 +296,35 @@ def test_parse_linked_times( @pytest.mark.parametrize( - "raw,value", - (("000", 0), ("999", 999), ("9500FT.", 9500)), + ("raw", "value"), + [("000", 0), ("999", 999), ("9500FT.", 9500)], ) -def test_make_altitude(raw: str, value: int): - """Tests altitude parsing""" +def test_make_altitude(raw: str, value: int) -> None: + """Test altitude parsing.""" altitude = notam.make_altitude(raw, structs.Units.international()) assert isinstance(altitude, structs.Number) assert altitude.value == value -@pytest.mark.parametrize("raw", ("", "G", "AGL")) -def test_bad_altitude(raw: str): - """Tests filtering bad altitude values""" +@pytest.mark.parametrize("raw", ["", "G", "AGL"]) +def test_bad_altitude(raw: str) -> None: + """Test filtering bad altitude values.""" assert notam.make_altitude(raw, structs.Units.international()) is None -def test_copied_tag(): - """Tests an instance when the body includes a previously used tag value""" - data = notam.Notams.from_report(COPIED_TAG_REPORT).data[0] +def test_copied_tag() -> None: + """Test an instance when the body includes a previously used tag value.""" + resp = notam.Notams.from_report(COPIED_TAG_REPORT) + assert resp is not None + assert resp.data is not None + data = resp.data[0] assert data.body.startswith("REF AIP") is True assert isinstance(data.end_time, structs.Code) assert data.end_time.repr == "PERM" -def test_parse(): - """Tests returned structs from the parse function""" +def test_parse() -> None: + """Test returned structs from the parse function.""" report = "01/113 NOTAMN \r\nQ) ZNY/QMXLC/IV/NBO/A/000/999/4038N07346W005 \r\nA) KJFK \r\nB) 2101081328 \r\nC) 2209301100 \r\n\r\nE) TWY TB BTN TERMINAL 8 RAMP AND TWY A CLSD" data, _ = notam.parse(report) assert isinstance(data, structs.NotamData) @@ -327,16 +332,17 @@ def test_parse(): @pytest.mark.parametrize( - "line,fixed", (("01/113 NOTAMN \r\nQ) 1234", "01/113 NOTAMN \nQ) 1234"),) + ("line", "fixed"), + [("01/113 NOTAMN \r\nQ) 1234", "01/113 NOTAMN \nQ) 1234")], ) -def test_sanitize(line: str, fixed: str): - """Tests report sanitization""" +def test_sanitize(line: str, fixed: str) -> None: + """Test report sanitization.""" assert notam.sanitize(line) == fixed -@pytest.mark.parametrize("ref,icao,_", get_data(__file__, "notam")) -def test_notam_e2e(ref: dict, icao: str, _): - """Performs an end-to-end test of all NOTAM JSON files""" +@pytest.mark.parametrize(("ref", "icao", "unused"), get_data(__file__, "notam")) +def test_notam_e2e(ref: dict, icao: str, unused: Any) -> None: # noqa: ARG001 + """Perform an end-to-end test of all NOTAM JSON files.""" station = notam.Notams(icao) assert station.last_updated is None reports = [report["data"]["raw"] for report in ref["reports"]] diff --git a/tests/current/test_pirep.py b/tests/current/test_pirep.py index 9e6a199..eeaadc6 100644 --- a/tests/current/test_pirep.py +++ b/tests/current/test_pirep.py @@ -1,13 +1,12 @@ -""" -PIREP Report Tests -""" +"""PIREP Report Tests.""" -# pylint: disable=protected-access,invalid-name +# ruff: noqa: SLF001 # stdlib +from __future__ import annotations + from dataclasses import asdict from datetime import datetime -from typing import List, Optional, Tuple # library import pytest @@ -16,29 +15,28 @@ from avwx import structs from avwx.current import pirep from avwx.structs import Code - from tests.util import assert_number, assert_value, get_data @pytest.mark.parametrize( - "root,station,report_type", - ( + ("root", "station", "report_type"), + [ ("JAX UA", "JAX", "UA"), ("DMW UUA", "DMW", "UUA"), ("UA", None, "UA"), ("MCO", "MCO", None), - ), + ], ) -def test_root(root: str, station: Optional[str], report_type: Optional[str]): - """Tests station and type identification""" +def test_root(root: str, station: str | None, report_type: str | None) -> None: + """Test station and type identification.""" ret_station, ret_report_type = pirep._root(root) assert ret_station == station assert ret_report_type == report_type @pytest.mark.parametrize( - "loc,station,direction,distance", - ( + ("loc", "station", "direction", "distance"), + [ ("MLB", "MLB", None, None), ("MKK360002", "MKK", 360, 2), ("KLGA220015", "KLGA", 220, 15), @@ -48,12 +46,10 @@ def test_root(root: str, station: Optional[str], report_type: Optional[str]): ("15 SW LRP", "LRP", 225, 15), ("3 MILES WEST OF MYF", "MYF", 270, 3), ("6 MILES EAST KAGC", "KAGC", 90, 6), - ), + ], ) -def test_location( - loc: str, station: Optional[str], direction: Optional[int], distance: Optional[int] -): - """Tests location unpacking""" +def test_location(loc: str, station: str | None, direction: int | None, distance: int | None) -> None: + """Test location unpacking.""" ret_loc = pirep._location(loc) assert isinstance(ret_loc, structs.Location) assert ret_loc.repr == loc @@ -62,59 +58,60 @@ def test_location( assert_value(ret_loc.distance, distance) -@pytest.mark.parametrize("time", ("1930", "0000", "2359", "0515")) -def test_time(time: str): - """Tests time parsing""" +@pytest.mark.parametrize("time", ["1930", "0000", "2359", "0515"]) +def test_time(time: str) -> None: + """Test time parsing.""" ret_time = pirep._time(time) assert isinstance(ret_time, structs.Timestamp) assert ret_time.repr == time + assert ret_time.dt is not None assert ret_time.dt.hour == int(time[:2]) assert ret_time.dt.minute == int(time[2:]) assert ret_time.dt.second == 0 -def test_not_time(): +def test_not_time() -> None: assert pirep._time(None) is None -@pytest.mark.parametrize("alt,num", (("2000", 2000), ("9999", 9999), ("0500", 500))) -def test_altitude(alt: str, num: int): - """Tests converting altitude to Number""" +@pytest.mark.parametrize(("alt", "num"), [("2000", 2000), ("9999", 9999), ("0500", 500)]) +def test_altitude(alt: str, num: int) -> None: + """Test converting altitude to Number.""" ret_alt = pirep._altitude(alt) assert isinstance(ret_alt, structs.Number) assert ret_alt.repr == alt assert ret_alt.value == num -def test_other_altitude(): +def test_other_altitude() -> None: assert isinstance(pirep._altitude("test"), str) assert pirep._altitude("") is None @pytest.mark.parametrize( - "code,name", - ( + ("code", "name"), + [ ("B752", "Boeing 757-200"), ("PC12", "Pilatus PC-12"), ("C172", "Cessna 172"), - ), + ], ) -def test_aircraft(code: str, name: str): - """Tests converting aircraft code into Aircraft""" +def test_aircraft(code: str, name: str) -> None: + """Test converting aircraft code into Aircraft.""" aircraft = pirep._aircraft(code) assert isinstance(aircraft, structs.Aircraft) assert aircraft.code == code assert aircraft.type == name -def test_unknown_aircraft(): +def test_unknown_aircraft() -> None: code = "not a code" assert pirep._aircraft(code) == code @pytest.mark.parametrize( - "cloud,data", - ( + ("cloud", "data"), + [ ("OVC100-TOP110", ["OVC", 100, 110]), ("OVC065-TOPUNKN", ["OVC", 65, None]), ("OVCUNKN-TOP150", ["OVC", None, 150]), @@ -128,10 +125,10 @@ def test_unknown_aircraft(): ("BASE027-TOPUNKN", [None, 27, None]), ("BASES020-TOPS074", [None, 20, 74]), ("BASES SCT022 TOPS SCT030-035", ["SCT", 22, 35]), - ), + ], ) -def test_cloud_tops(cloud: str, data: list): - """Tests converting clouds with tops""" +def test_cloud_tops(cloud: str, data: list) -> None: + """Test converting clouds with tops.""" parsed = pirep._clouds(cloud)[0] assert isinstance(parsed, structs.Cloud) assert parsed.repr == cloud @@ -140,40 +137,38 @@ def test_cloud_tops(cloud: str, data: list): @pytest.mark.parametrize( - "num,value", - ( + ("num", "value"), + [ ("01", 1), ("M01", -1), ("E", 90), ("1/4", 0.25), ("28C", 28), - ), + ], ) -def test_number(num: str, value: int): - """Tests converting value into a Number""" +def test_number(num: str, value: int) -> None: + """Test converting value into a Number.""" assert_number(pirep._number(num), num, value) -@pytest.mark.parametrize("bad", ("", "LGT RIME")) -def test_not_number(bad: str): +@pytest.mark.parametrize("bad", ["", "LGT RIME"]) +def test_not_number(bad: str) -> None: assert pirep._number(bad) is None @pytest.mark.parametrize( - "turb,severity,floor,ceiling", - ( + ("turb", "severity", "floor", "ceiling"), + [ ("MOD CHOP", "MOD CHOP", None, None), ("LGT-MOD CAT 160-260", "LGT-MOD CAT", 160, 260), ("LGT-MOD 180-280", "LGT-MOD", 180, 280), ("LGT", "LGT", None, None), ("CONT LGT CHOP BLO 250", "CONT LGT CHOP", None, 250), ("LGT-NEG-MOD BLO 090-075", "LGT-NEG-MOD", 75, 90), - ), + ], ) -def test_turbulence( - turb: str, severity: str, floor: Optional[int], ceiling: Optional[int] -): - """Tests converting turbulence string to Turbulence""" +def test_turbulence(turb: str, severity: str, floor: int | None, ceiling: int | None) -> None: + """Test converting turbulence string to Turbulence.""" ret_turb = pirep._turbulence(turb) assert isinstance(ret_turb, structs.Turbulence) assert ret_turb.severity == severity @@ -182,24 +177,24 @@ def test_turbulence( @pytest.mark.parametrize( - "ice,severity,itype,floor,ceiling", - ( + ("ice", "severity", "itype", "floor", "ceiling"), + [ ("MOD RIME", "MOD", "RIME", None, None), ("LGT RIME 025", "LGT", "RIME", 25, 25), ("LIGHT MIXED", "LIGHT", "MIXED", None, None), ("NEG", "NEG", None, None, None), ("TRACE RIME 070-090", "TRACE", "RIME", 70, 90), ("LGT RIME 220-260", "LGT", "RIME", 220, 260), - ), + ], ) def test_icing( ice: str, severity: str, - itype: Optional[str], - floor: Optional[int], - ceiling: Optional[int], -): - """Tests converting icing string to Icing""" + itype: str | None, + floor: int | None, + ceiling: int | None, +) -> None: + """Test converting icing string to Icing.""" ret_ice = pirep._icing(ice) assert isinstance(ret_ice, structs.Icing) assert ret_ice.severity == severity @@ -208,17 +203,17 @@ def test_icing( assert_value(ret_ice.ceiling, ceiling) -@pytest.mark.parametrize("rmk", ("Test", "12345", "IT WAS MOSTLY SMOOTH")) -def test_remarks(rmk: str): - """Tests remarks pass through""" +@pytest.mark.parametrize("rmk", ["Test", "12345", "IT WAS MOSTLY SMOOTH"]) +def test_remarks(rmk: str) -> None: + """Test remarks pass through.""" ret_rmk = pirep._remarks(rmk) assert isinstance(ret_rmk, str) assert ret_rmk, rmk @pytest.mark.parametrize( - "text,wx,vis,remain", - ( + ("text", "wx", "vis", "remain"), + [ ("VCFC", [("VCFC", "Vicinity Funnel Cloud")], None, []), ( "+RATS -GR 4", @@ -227,12 +222,10 @@ def test_remarks(rmk: str): ["4"], ), ("FV1000 VCFC", [("VCFC", "Vicinity Funnel Cloud")], 1000, []), - ), + ], ) -def test_wx( - text: str, wx: List[Tuple[str, str]], vis: Optional[int], remain: List[str] -): - """Tests wx split and visibility ident""" +def test_wx(text: str, wx: list[tuple[str, str]], vis: int | None, remain: list[str]) -> None: + """Test wx split and visibility ident.""" wx_codes, flight_visibility, other = pirep._wx(text) assert isinstance(wx_codes, list) assert other == remain @@ -246,13 +239,13 @@ def test_wx( @pytest.mark.parametrize( "report", - ( + [ "EWR UA /OV SBJ090/010/TM 2108/FL060/TP B738/TB MOD", "SMQ UA /OV BWZ/TM 0050/FL280/TP A320/TB MOD", - ), + ], ) -def test_parse(report: str): - """Tests returned structs from the parse function""" +def test_parse(report: str) -> None: + """Test returned structs from the parse function.""" data, sans = pirep.parse(report) assert isinstance(data, structs.PirepData) assert isinstance(sans, structs.Sanitization) @@ -260,22 +253,22 @@ def test_parse(report: str): @pytest.mark.parametrize( - "line,fixed", - ( + ("line", "fixed"), + [ ("DAB UA /SK BKN030 TOP045", "DAB UA /SK BKN030-TOP045"), ("DAB UA /SK BASES OVC 049 TOPS 055", "DAB UA /SK BASES OVC049 TOPS 055"), - ), + ], ) -def test_sanitize(line: str, fixed: str): - """Tests report sanitization""" +def test_sanitize(line: str, fixed: str) -> None: + """Test report sanitization.""" ret_fixed, sans = pirep.sanitize(line) assert ret_fixed == fixed assert sans.errors_found is True -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "pirep")) -def test_pirep_ete(ref: dict, icao: str, issued: datetime): - """Performs an end-to-end test of all PIREP JSON files""" +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "pirep")) +def test_pirep_ete(ref: dict, icao: str, issued: datetime) -> None: + """Perform an end-to-end test of all PIREP JSON files.""" station = pirep.Pireps(icao) assert station.last_updated is None assert station.issued is None diff --git a/tests/current/test_taf.py b/tests/current/test_taf.py index 94def5e..10aa02f 100644 --- a/tests/current/test_taf.py +++ b/tests/current/test_taf.py @@ -1,16 +1,16 @@ -""" -TAF Report Tests -""" +"""TAF Report Tests.""" -# pylint: disable=protected-access,invalid-name +# ruff: noqa: SLF001 # stdlib +from __future__ import annotations + import json from copy import deepcopy from dataclasses import asdict from datetime import datetime from pathlib import Path -from typing import List, Optional +from typing import Any # library import pytest @@ -19,10 +19,8 @@ from avwx import static, structs from avwx.current import taf from avwx.parsing import core - from tests.util import get_data - DATA_DIR = Path(__file__).parent / "data" @@ -51,24 +49,22 @@ def line_struct(fields: dict) -> structs.TafLineData: - # sourcery skip: dict-assign-update-to-union - """Create TafLineData with null missing fields""" - existing = {k: None for k in TAF_FIELDS} - existing.update(fields) - return structs.TafLineData(**existing) + """Create TafLineData with null missing fields.""" + fields = {k: None for k in TAF_FIELDS} | fields + return structs.TafLineData(**fields) @pytest.mark.parametrize( - "text,rmk", - ( + ("text", "rmk"), + [ ("KJFK test", ""), ("KJFK test RMK test", "RMK test"), ("KJFK test FCST test", "FCST test"), ("KJFK test AUTOMATED test", "AUTOMATED test"), - ), + ], ) -def test_get_taf_remarks(text: str, rmk: str): - """Tests that remarks are removed from a TAF report""" +def test_get_taf_remarks(text: str, rmk: str) -> None: + """Test that remarks are removed from a TAF report.""" report, remarks = taf.get_taf_remarks(text) assert report == "KJFK test" assert remarks == rmk @@ -78,8 +74,8 @@ def test_get_taf_remarks(text: str, rmk: str): @pytest.mark.parametrize("case", json.load(LINE_TEST_PATH.open())) -def test_sanitize_line(case: dict): - """Tests a function which fixes common new-line signifiers in TAF reports""" +def test_sanitize_line(case: dict) -> None: + """Test a function which fixes common new-line signifiers in TAF reports.""" sans = structs.Sanitization() fixed = taf.sanitize_line(case["line"], sans) assert fixed == case["fixed"] @@ -89,63 +85,59 @@ def test_sanitize_line(case: dict): @pytest.mark.parametrize( "line", - ( + [ {"type": "TEMPO"}, {"probability": 30}, {"probability": "PROBNA"}, {"type": "FROM", "probability": 30}, - ), + ], ) -def test_is_tempo_or_prob(line: dict): - """Tests a function which checks that an item signifies a new time period""" +def test_is_tempo_or_prob(line: dict) -> None: + """Test a function which checks that an item signifies a new time period.""" assert taf._is_tempo_or_prob(line_struct(line)) is True -@pytest.mark.parametrize( - "line", ({"type": "FROM"}, {"type": "FROM", "probability": None}) -) -def test_is_not_tempo_or_prob(line: dict): +@pytest.mark.parametrize("line", [{"type": "FROM"}, {"type": "FROM", "probability": None}]) +def test_is_not_tempo_or_prob(line: dict) -> None: assert taf._is_tempo_or_prob(line_struct(line)) is False @pytest.mark.parametrize( - "wx,alt,ice,turb", - ( + ("wx", "alt", "ice", "turb"), + [ (["1"], None, [], []), (["1", "512345", "612345"], None, ["612345"], ["512345"]), (["QNH1234", "1", "612345"], core.make_number("1234"), ["612345"], []), - ), + ], ) -def test_get_alt_ice_turb( - wx: List[str], alt: Optional[core.Number], ice: List[str], turb: List[str] -): - """Tests that report global altimeter, icing, and turbulence get removed""" +def test_get_alt_ice_turb(wx: list[str], alt: core.Number | None, ice: list[str], turb: list[str]) -> None: + """Test that report global altimeter, icing, and turbulence get removed.""" assert taf.get_alt_ice_turb(wx) == (["1"], alt, ice, turb) @pytest.mark.parametrize( "item", - ( + [ *static.taf.TAF_NEWLINE, "PROB30", "PROB45", "PROBNA", "FM12345678", - ), + ], ) -def test_starts_new_line(item: str): - """Tests that certain items are identified as new line markers in TAFs""" +def test_starts_new_line(item: str) -> None: + """Test that certain items are identified as new line markers in TAFs.""" assert taf.starts_new_line(item) is True -@pytest.mark.parametrize("item", ("KJFK", "12345Z", "2010/2020", "FEW060", "RMK")) -def test_doesnt_start_new_line(item: str): +@pytest.mark.parametrize("item", ["KJFK", "12345Z", "2010/2020", "FEW060", "RMK"]) +def test_doesnt_start_new_line(item: str) -> None: assert taf.starts_new_line(item) is False @pytest.mark.parametrize( - "report,num", - ( + ("report", "num"), + [ ("KJFK test", 1), ("KJFK test FM12345678 test", 2), ("KJFK test TEMPO test", 2), @@ -156,18 +148,18 @@ def test_doesnt_start_new_line(item: str): "KJFK test 2612/2712 test PROB40 2612/2618 test 2618/2706 test 2706/2709 test 2709/2712 test", 6, ), - ), + ], ) -def test_split_taf(report: str, num: int): - """Tests that TAF reports are split into the correct time periods""" +def test_split_taf(report: str, num: int) -> None: + """Test that TAF reports are split into the correct time periods.""" split = taf.split_taf(report) assert len(split) == num assert split[0] == "KJFK test" @pytest.mark.parametrize( - "wx,data", - ( + ("wx", "data"), + [ (["1"], ["FROM", None, None, None]), (["INTER", "1"], ["INTER", None, None, None]), (["TEMPO", "0101/0103", "1"], ["TEMPO", "0101", "0103", None]), @@ -177,16 +169,16 @@ def test_split_taf(report: str, num: int): (["FM1200/1206", "1"], ["FROM", "1200", "1206", None]), (["FM120000", "TL120600", "1"], ["FROM", "1200", "1206", None]), (["TEMPO", "112000", "1"], ["TEMPO", "1120", None, None]), - ), + ], ) -def test_get_type_and_times(wx: List[str], data: List[Optional[str]]): - """Tests TAF line type, start time, and end time extraction""" +def test_get_type_and_times(wx: list[str], data: list[str | None]) -> None: + """Test TAF line type, start time, and end time extraction.""" assert taf.get_type_and_times(wx) == (["1"], *data) -def test_find_missing_taf_times(): - """Tests that missing forecast times can be interpretted by preceding periods""" - good_lines = [ +def test_find_missing_taf_times() -> None: + """Test that missing forecast times can be interpretted by preceding periods.""" + good_data: list[dict] = [ { "type": "FROM", "transition_start": None, @@ -212,11 +204,11 @@ def test_find_missing_taf_times(): "end_time": "0114", }, ] - for line in good_lines: + for line in good_data: for key in ("start_time", "end_time", "transition_start"): line[key] = core.make_timestamp(line[key]) - good_lines = [line_struct(line) for line in good_lines] - bad_lines = deepcopy(good_lines) + good_lines = [line_struct(line) for line in good_data] + bad_lines: list[structs.TafLineData] = deepcopy(good_lines) bad_lines[0].start_time = None bad_lines[1].start_time = None bad_lines[1].end_time = None @@ -228,35 +220,34 @@ def test_find_missing_taf_times(): @pytest.mark.parametrize( "report", - ( + [ "SYCJ 071100Z 0712/0812 12003KT 9999 BECMG 0711/ 12003KT 1000 TEMPO 0717/0720 12003KT 5000", - ), + ], ) -def test_missing_end_times(report: str): - """Find missing times from a report string""" +def test_missing_end_times(report: str) -> None: + """Find missing times from a report string.""" data, _, _ = taf.parse(report[:4], report) + assert data is not None for line in data.forecast: assert isinstance(line.start_time, structs.Timestamp) assert isinstance(line.end_time, structs.Timestamp) @pytest.mark.parametrize( - "wx,temp_max,temp_min", - ( + ("wx", "temp_max", "temp_min"), + [ (["1"], None, None), (["1", "TX12/1316Z", "TNM03/1404Z"], "TX12/1316Z", "TNM03/1404Z"), (["1", "TM03/1404Z", "T12/1316Z"], "TX12/1316Z", "TNM03/1404Z"), - ), + ], ) -def test_get_temp_min_and_max( - wx: List[str], temp_max: Optional[str], temp_min: Optional[str] -): - """Tests that temp max and min times are extracted and assigned properly""" +def test_get_temp_min_and_max(wx: list[str], temp_max: str | None, temp_min: str | None) -> None: + """Test that temp max and min times are extracted and assigned properly.""" assert taf.get_temp_min_and_max(wx) == (["1"], temp_max, temp_min) -def test_get_oceania_temp_and_alt(): - """Tests that Oceania-specific elements are identified and removed""" +def test_get_oceania_temp_and_alt() -> None: + """Test that Oceania-specific elements are identified and removed.""" items = ["1", "T", "2", "3", "ODD", "Q", "4", "C"] items, tlist, qlist = taf.get_oceania_temp_and_alt(items) assert items == ["1", "ODD", "C"] @@ -265,19 +256,19 @@ def test_get_oceania_temp_and_alt(): @pytest.mark.parametrize( - "wx,shear", - ( + ("wx", "shear"), + [ (["1", "2"], None), (["1", "2", "WS020/07040"], "WS020/07040"), - ), + ], ) -def test_get_wind_shear(wx: List[str], shear: Optional[str]): - """Tests extracting wind shear""" +def test_get_wind_shear(wx: list[str], shear: str | None) -> None: + """Test extracting wind shear.""" assert taf.get_wind_shear(wx) == (["1", "2"], shear) -def test_skc_flight_rules(): - """SKC should prevent temp_cloud lookback instead of missing clouds""" +def test_skc_flight_rules() -> None: + """SKC should prevent temp_cloud lookback instead of missing clouds.""" report = ( "KLGB 201120Z 2012/2112 VRB03KT P6SM OVC016 " "FM201600 15006KT P6SM BKN021 " @@ -292,8 +283,8 @@ def test_skc_flight_rules(): assert period.flight_rules == flight_rules -def test_parse(): - """Tests returned structs from the parse function""" +def test_parse() -> None: + """Test returned structs from the parse function.""" report = ( "PHNL 042339Z 0500/0606 06018G25KT P6SM FEW030 SCT060 FM050600 06010KT " "P6SM FEW025 SCT060 FM052000 06012G20KT P6SM FEW030 SCT060" @@ -305,7 +296,7 @@ def test_parse(): assert data.raw == report -def test_prob_line(): +def test_prob_line() -> None: """Even though PROB__ is not in TAF_NEWLINE, it should still separate, add a new line, and include the prob value in line.probability """ @@ -318,7 +309,8 @@ def test_prob_line(): "FM280000 23008KT P6SM BKN040 RMK FCST BASED ON AUTO OBS. NXT FCST BY 272000Z" ) tafobj = taf.Taf("CYBC") - tafobj.parse(report) + assert tafobj.parse(report) + assert tafobj.data is not None lines = tafobj.data.forecast assert len(lines) == 6 assert lines[3].probability is None @@ -326,8 +318,13 @@ def test_prob_line(): assert lines[4].raw.startswith("PROB30") is True -def test_prob_end(): - """PROB and TEMPO lines are discrete times and should not affect FROM times""" +def assert_repr(time: structs.Timestamp | Any | None, repr: str) -> None: # noqa: A002 + assert isinstance(time, structs.Timestamp) + assert time.repr == repr + + +def test_prob_end() -> None: + """PROB and TEMPO lines are discrete times and should not affect FROM times.""" report = ( "MMMX 242331Z 2500/2606 24005KT P6SM VC RA SCT020CB SCT080 BKN200 TX25/2521Z TN14/2513Z " "TEMPO 2500/2504 6SM TSRA BKN020CB " @@ -338,21 +335,22 @@ def test_prob_end(): "PROB40 2522/2602 P6SM TSRA BKN020CB" ) tafobj = taf.Taf("CYBC") - tafobj.parse(report) + assert tafobj.parse(report) + assert tafobj.data is not None lines = tafobj.data.forecast - assert lines[0].start_time.repr == "2500" - assert lines[0].end_time.repr == "2506" - assert lines[1].start_time.repr == "2500" - assert lines[1].end_time.repr == "2504" - assert lines[-2].start_time.repr == "2518" - assert lines[-2].end_time.repr == "2606" - assert lines[-1].start_time.repr == "2522" - assert lines[-1].end_time.repr == "2602" + assert_repr(lines[0].start_time, "2500") + assert_repr(lines[0].end_time, "2506") + assert_repr(lines[1].start_time, "2500") + assert_repr(lines[1].end_time, "2504") + assert_repr(lines[-2].start_time, "2518") + assert_repr(lines[-2].end_time, "2606") + assert_repr(lines[-1].start_time, "2522") + assert_repr(lines[-1].end_time, "2602") @pytest.mark.parametrize( - "report,fixed", - ( + ("report", "fixed"), + [ ( "KNGU TAF COR 2315/2415 18011KT 9999 VCTS", "TAF COR KNGU 2315/2415 18011KT 9999 VCTS", @@ -364,15 +362,15 @@ def test_prob_end(): ("1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8"), ("1 2 TAF 3 4 5 6 7 8 COR 9", "TAF 1 2 3 4 5 6 7 8 COR 9"), ("", ""), - ), + ], ) -def test_bad_header(report: str, fixed: str): - """Should fix the header order for key elements ignoring copies later on""" +def test_bad_header(report: str, fixed: str) -> None: + """Should fix the header order for key elements ignoring copies later on.""" assert taf.fix_report_header(report) == fixed -def test_wind_shear(): - """Wind shear should be recognized as its own element in addition to wind""" +def test_wind_shear() -> None: + """Wind shear should be recognized as its own element in addition to wind.""" report = ( "TAF AMD CYOW 282059Z 2821/2918 09008KT WS015/20055KT P6SM BKN220 " "BECMG 2821/2823 19015G25KT " @@ -383,22 +381,25 @@ def test_wind_shear(): "TEMPO 2913/2918 P6SM -SHRA OVC020 RMK NXT FCST BY 290000Z" ) tafobj = taf.Taf("CYOW") - tafobj.parse(report) + assert tafobj.parse(report) + assert tafobj.data is not None lines = tafobj.data.forecast assert len(lines) == 7 assert lines[0].wind_shear == "WS015/20055" + assert tafobj.translations is not None assert tafobj.translations.forecast[1].clouds == "" -def test_space_in_forcast_times(): - """Forecast time can occasionally use a space rather than a slash""" +def test_space_in_forcast_times() -> None: + """Forecast time can occasionally use a space rather than a slash.""" report = ( "TAF VIGR 041530Z 0500/0512 04005KT 6000 SCT025 BKN090 " "0501 0503 05010KT 4000 -RA/BR SCT015 SCT025 BKN090 " "TEMPO 0507 0511 05010G20KT 2000 RASH/TS SCT025 FEWCB035 SCT090" ) tafobj = taf.Taf("VIGR") - tafobj.parse(report) + assert tafobj.parse(report) + assert tafobj.data is not None lines = tafobj.data.forecast assert len(lines) == 2 assert lines[0].start_time == core.make_timestamp("0500") @@ -407,39 +408,46 @@ def test_space_in_forcast_times(): assert lines[1].end_time == core.make_timestamp("0511") -def test_wind_variable_direction(): - """Variable wind direction should be recognized when present""" +def test_wind_variable_direction() -> None: + """Variable wind direction should be recognized when present.""" report = ( "MNPC 301530Z 3018/3118 16006KT 100V200 5000 RA/TSRA FEW014CB BKN016TCU " "BECMG 3100/3102 VRB04KT 6000 -RA/HZ BKN016" ) tafobj = taf.Taf.from_report(report) + assert tafobj is not None + assert tafobj.data is not None lines = tafobj.data.forecast assert len(lines) == 2 - assert len(lines[0].wind_variable_direction) == 2 + wind_direction = lines[0].wind_variable_direction + assert wind_direction is not None + assert len(wind_direction) == 2 -def test_prob_tempo(): - """Non-PROB types should take precident but still fill the probability value""" +def test_prob_tempo() -> None: + """Non-PROB types should take precident but still fill the probability value.""" report = ( "EGLL 192253Z 2000/2106 28006KT 9999 BKN035 " "PROB30 TEMPO 2004/2009 BKN012 " "PROB30 TEMPO 2105/2106 8000 BKN006" ) tafobj = taf.Taf("EGLL") - tafobj.parse(report) + assert tafobj.parse(report) + assert tafobj.data is not None lines = tafobj.data.forecast + assert len(lines) == 3 for line in lines: assert isinstance(line.start_time, structs.Timestamp) assert isinstance(line.end_time, structs.Timestamp) - for i in range(1, 3): - assert lines[i].type == "TEMPO" - assert lines[i].probability.value == 30 + for line in lines[1:4]: + assert line.type == "TEMPO" + assert isinstance(line.probability, structs.Number) + assert line.probability.value == 30 -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "taf")) -def test_taf_ete(ref: dict, icao: str, issued: datetime): - """Performs an end-to-end test of all TAF JSON files""" +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "taf")) +def test_taf_ete(ref: dict, icao: str, issued: datetime) -> None: + """Perform an end-to-end test of all TAF JSON files.""" station = taf.Taf(icao) assert station.last_updated is None assert station.issued is None @@ -454,8 +462,8 @@ def test_taf_ete(ref: dict, icao: str, issued: datetime): assert station.speech == ref["speech"] -def test_rule_inherit(): - """Tests if TAF forecast periods selectively inherit features to calculate flight rules""" +def test_rule_inherit() -> None: + """Test if TAF forecast periods selectively inherit features to calculate flight rules.""" report = ( "CYKF 020738Z 0208/0220 34005KT P6SM FEW015 BKN070 " "FM020900 VRB03KT P6SM FEW070 SCT120 " @@ -464,5 +472,7 @@ def test_rule_inherit(): ) expected_rules = ("VFR", "VFR", "VFR", "MVFR") tafobj = taf.Taf.from_report(report) + assert isinstance(tafobj, taf.Taf) + assert tafobj.data is not None for i, line in enumerate(tafobj.data.forecast): assert line.flight_rules == expected_rules[i] diff --git a/tests/forecast/_test_gfs.py b/tests/forecast/_test_gfs.py index 1fcb9f1..2632f40 100644 --- a/tests/forecast/_test_gfs.py +++ b/tests/forecast/_test_gfs.py @@ -1,8 +1,6 @@ -""" -GFS service forecast parsing tests -""" +"""GFS service forecast parsing tests.""" -# pylint: disable=protected-access,missing-class-docstring +# ruff: noqa: SLF001 # library import pytest @@ -12,26 +10,28 @@ # tests from tests.util import assert_number, get_data + from .test_base import ForecastBase -def test_thunder(): - """Tests that a line is converted into Number tuples""" +def test_thunder() -> None: + """Test that a line is converted into Number tuples.""" text = "T06 12/16 0/ 5" _values = (None, None, (("12", 12), ("16", 16)), None, (("0", 0), ("5", 5))) for codes, values in zip(gfs._thunder(text), _values): if values is None: assert codes is None else: + assert codes is not None for code, value in zip(codes, values): assert_number(code, *value) -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "mav")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "mav")) class TestMav(ForecastBase): report = gfs.Mav -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "mex")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "mex")) class TestMex(ForecastBase): report = gfs.Mex diff --git a/tests/forecast/test_base.py b/tests/forecast/test_base.py index 5bcc172..d9f90aa 100644 --- a/tests/forecast/test_base.py +++ b/tests/forecast/test_base.py @@ -1,13 +1,12 @@ -""" -Forecast module parsing tests -""" +"""Forecast module parsing tests.""" -# pylint: disable=protected-access,missing-class-docstring +# ruff: noqa: SLF001 # stdlib +from __future__ import annotations + from dataclasses import asdict from datetime import datetime, timedelta, timezone -from typing import List, Optional, Tuple, Union # library import pytest @@ -19,7 +18,6 @@ # tests from tests.util import assert_code, assert_number, assert_timestamp - MAV_HEAD = """ KMCO GFS MOS GUIDANCE 2/11/2020 0000 UTC DT /FEB 11 /FEB 12 /FEB 13 / @@ -34,9 +32,9 @@ """ -@pytest.mark.parametrize("target,length", ((0, 47), (1, 67), (2, 68))) -def test_trim_lines(target: int, length: int): - """Tests trimming line length based on the length of a target line""" +@pytest.mark.parametrize(("target", "length"), [(0, 47), (1, 67), (2, 68)]) +def test_trim_lines(target: int, length: int) -> None: + """Test trimming line length based on the length of a target line.""" lines = base._trim_lines(MAV_HEAD.strip().split("\n"), target) assert len(lines) == 3 for line in lines: @@ -44,21 +42,21 @@ def test_trim_lines(target: int, length: int): @pytest.mark.parametrize( - "size,prefix,expect", - ( + ("size", "prefix", "expect"), + [ (3, 0, ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]), (3, 3, ["2", "3", "4", "5", "6", "7", "8", "9", "0"]), (4, 0, ["1", "2 3", "4", "5", "6 7", "8", "9", "0"]), (2, 7, ["3", "4", "", "5", "6", "", "7", "8", "", "9", "0"]), - ), + ], ) -def test_split_line(size: int, prefix: int, expect: List[str]): - """Tests splitting and stripping elements on a non-delineated line""" +def test_split_line(size: int, prefix: int, expect: list[str]) -> None: + """Test splitting and stripping elements on a non-delineated line.""" text = " 1 2 3 4 5 6 7 8 9 0 " assert base._split_line(text, size, prefix) == expect -def test_split_mav_line(): +def test_split_mav_line() -> None: text = "HR 06 09 12 15 18 21 00 03 06 09 12 15 18 21 00 03 06 09 12 18 00 " line = base._split_line(text) assert len(line) == 21 @@ -66,7 +64,7 @@ def test_split_mav_line(): assert line[-1] == "00" -def test_split_mex_line(): +def test_split_mex_line() -> None: text = "FHR 24 36| 48 60| 72 84| 96 108|120 132|144 156|168 180|192" line = base._split_line(text, size=4) assert len(line) == 15 @@ -75,17 +73,18 @@ def test_split_mex_line(): @pytest.mark.parametrize( - "year,month,day,hour,minute,line", - ( + ("year", "month", "day", "hour", "minute", "line"), + [ (2020, 2, 11, 0, 0, "KMCO GFS MOS GUIDANCE 2/11/2020 0000 UTC"), (2020, 12, 3, 12, 0, "KMCO GFSX MOS GUIDANCE 12/03/2020 1200 UTC"), - ), + ], ) -def test_timestamp(year: int, month: int, day: int, hour: int, minute: int, line: str): - """Tests Timestamp inference on the first line of the report""" +def test_timestamp(year: int, month: int, day: int, hour: int, minute: int, line: str) -> None: + """Test Timestamp inference on the first line of the report.""" time = base._timestamp(line) assert isinstance(time, structs.Timestamp) assert isinstance(time.repr, str) + assert time.dt is not None assert time.dt.year == year assert time.dt.month == month assert time.dt.day == day @@ -95,13 +94,11 @@ def test_timestamp(year: int, month: int, day: int, hour: int, minute: int, line @pytest.mark.parametrize( - "counters,line", - ( + ("counters", "line"), + [ ( [(18, 3), (2, 6)], - base._split_line( - "HR 06 09 12 15 18 21 00 03 06 09 12 15 18 21 00 03 06 09 12 18 00 " - ), + base._split_line("HR 06 09 12 15 18 21 00 03 06 09 12 15 18 21 00 03 06 09 12 18 00 "), ), ( [(14, 12)], @@ -110,10 +107,10 @@ def test_timestamp(year: int, month: int, day: int, hour: int, minute: int, line size=4, ), ), - ), + ], ) -def test_find_time_periods(counters: List[Tuple[int, int]], line: List[str]): - """Tests creating datetime objects from a line of hours-since values""" +def test_find_time_periods(counters: list[tuple[int, int]], line: list[str]) -> None: + """Test creating datetime objects from a line of hours-since values.""" start_time = datetime(2020, 2, 11, 0, 0, tzinfo=timezone.utc) times = base._find_time_periods(line, start_time) time = start_time + timedelta(hours=int(line[0])) @@ -129,14 +126,14 @@ def test_find_time_periods(counters: List[Tuple[int, int]], line: List[str]): @pytest.mark.parametrize( - "report,time,line_length", - ( + ("report", "time", "line_length"), + [ (MAV_HEAD, datetime(2020, 2, 11, 0, 0, tzinfo=timezone.utc), 3), (NBS_HEAD, datetime(2020, 7, 19, 16, 0, tzinfo=timezone.utc), 4), - ), + ], ) -def test_init_parse(report: str, time: datetime, line_length: int): - """Tests first pass data creation and line splits""" +def test_init_parse(report: str, time: datetime, line_length: int) -> None: + """Test first pass data creation and line splits.""" data, lines = base._init_parse(report) assert isinstance(data, structs.ReportData) assert isinstance(lines, list) @@ -149,34 +146,34 @@ def test_init_parse(report: str, time: datetime, line_length: int): @pytest.mark.parametrize( - "numbers,line,prefix,postfix,decimal", - ( + ("numbers", "line", "prefix", "postfix", "decimal"), + [ ([1, 2, None, 34], "NUM 1 2 34", "", "", None), ([10, 20, None, 340], "NUM 1 2 34", "", "0", None), ([110, 120, None, 1340], "NUM 1 2 34", "1", "0", None), ([1.1, 1.2, None, 13.4], "NUM 1 2 34", "1", "", -1), - ), + ], ) def test_numbers( - numbers: List[Union[int, float, None]], + numbers: list[float | None], line: str, prefix: str, postfix: str, - decimal: Optional[int], -): - """Tests that number lines are converted to Number or None""" + decimal: int | None, +) -> None: + """Test that number lines are converted to Number or None.""" raw = line[4:] - raw = [raw[i : (i + 3)].strip() for i in range(0, len(raw), 3)] - for i, number in enumerate(base._numbers(line, 3, prefix, postfix, decimal)): - num, text = numbers[i], raw[i] + items = [raw[i : (i + 3)].strip() for i in range(0, len(raw), 3)] + for i, number in enumerate(base._numbers(line, 3, prefix, postfix, decimal=decimal)): + num, text = numbers[i], items[i] if num is None: assert number is None else: assert_number(number, text, num) -def test_code(): - """Tests that codes are properly mapped into Code dataclasses""" +def test_code() -> None: + """Test that codes are properly mapped into Code dataclasses.""" codes = {"A": 1, "B": 2} text = "COD A B C" values = (("A", 1), ("B", 2), (None, None), ("C", "C")) @@ -185,14 +182,13 @@ def test_code(): class ForecastBase: - report: base.Forecast + report: type[base.Forecast] - def test_forecast_ete(self, ref: dict, icao: str, issued: datetime): - """Performs an end-to-end test of all report JSON files""" + def test_forecast_ete(self, ref: dict, icao: str, issued: datetime) -> None: + """Perform an end-to-end test of all report JSON files.""" station = self.report(icao) assert station.last_updated is None assert station.issued is None - print(ref["data"]["raw"]) assert station.parse(ref["data"]["raw"]) is True assert isinstance(station.last_updated, datetime) assert station.issued == issued diff --git a/tests/forecast/test_nbm.py b/tests/forecast/test_nbm.py index 1577755..6cb7e79 100644 --- a/tests/forecast/test_nbm.py +++ b/tests/forecast/test_nbm.py @@ -1,8 +1,6 @@ -""" -NBM service forecast parsing tests -""" +"""NBM service forecast parsing tests.""" -# pylint: disable=protected-access,missing-class-docstring +# ruff: noqa: SLF001 # library import pytest @@ -12,11 +10,12 @@ # tests from tests.util import assert_number, get_data + from .test_base import ForecastBase -def test_ceiling(): - """Tests that a line is converted into ceiling-specific Numbers""" +def test_ceiling() -> None: + """Test that a line is converted into ceiling-specific Numbers.""" line = "CIG 12888 45" values = [ ("12", 1200, "one two hundred"), @@ -31,8 +30,8 @@ def test_ceiling(): assert_number(number, *expected) -def test_wind(): - """Tests that a line is converted into wind-specific Numbers""" +def test_wind() -> None: + """Test that a line is converted into wind-specific Numbers.""" line = "GST 12 NG 45" values = [ ("12", 12, "one two"), @@ -47,21 +46,21 @@ def test_wind(): assert_number(number, *expected) -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "nbh")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "nbh")) class TestNbh(ForecastBase): report = nbm.Nbh -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "nbs")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "nbs")) class TestNbs(ForecastBase): report = nbm.Nbs -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "nbe")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "nbe")) class TestNbe(ForecastBase): report = nbm.Nbe -@pytest.mark.parametrize("ref,icao,issued", get_data(__file__, "nbx")) +@pytest.mark.parametrize(("ref", "icao", "issued"), get_data(__file__, "nbx")) class TestNbx(ForecastBase): report = nbm.Nbx diff --git a/tests/parsing/cleaners/test_cleaners.py b/tests/parsing/cleaners/test_cleaners.py index c8cbd4d..82e34e8 100644 --- a/tests/parsing/cleaners/test_cleaners.py +++ b/tests/parsing/cleaners/test_cleaners.py @@ -1,42 +1,41 @@ -""" -Other cleaner tests -""" +"""Other cleaner tests.""" import pytest -from avwx.parsing.sanitization.cleaners import cleaners, base as abc +from avwx.parsing.sanitization.cleaners import base as abc +from avwx.parsing.sanitization.cleaners import cleaners -@pytest.mark.parametrize("item", ("/" * i for i in range(1, 8))) -def test_only_slashes(item: str): +@pytest.mark.parametrize("item", ["/" * i for i in range(1, 8)]) +def test_only_slashes(item: str) -> None: assert cleaners.OnlySlashes().can_handle(item) is True -@pytest.mark.parametrize("item", ("/////KT", "KMCO", "//SM", "TX12/1234")) -def test_not_only_slashes(item: str): +@pytest.mark.parametrize("item", ["/////KT", "KMCO", "//SM", "TX12/1234"]) +def test_not_only_slashes(item: str) -> None: assert cleaners.OnlySlashes().can_handle(item) is False @pytest.mark.parametrize( - "item,clean", - ( + ("item", "clean"), + [ ("REVCTS", "VCTS"), ("REBR", "BR"), - ), + ], ) -def test_trim_wx_code(item: str, clean: str): +def test_trim_wx_code(item: str, clean: str) -> None: cleaner = cleaners.TrimWxCode() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("REMARKS", "RE", "REF", "RECON", "REQ", "BR", "VCTS")) -def test_not_trim_wx_code(item: str): +@pytest.mark.parametrize("item", ["REMARKS", "RE", "REF", "RECON", "REQ", "BR", "VCTS"]) +def test_not_trim_wx_code(item: str) -> None: assert cleaners.TrimWxCode().can_handle(item) is False -def test_cleaner_abc(): - """Test that cleaner base classes don't implement core methods""" +def test_cleaner_abc() -> None: + """Test that cleaner base classes don't implement core methods.""" for cleaner in (abc.SingleItem, abc.RemoveItem, abc.CleanItem): with pytest.raises(NotImplementedError): cleaner().can_handle("") diff --git a/tests/parsing/cleaners/test_cloud.py b/tests/parsing/cleaners/test_cloud.py index 1cef347..ad07e74 100644 --- a/tests/parsing/cleaners/test_cloud.py +++ b/tests/parsing/cleaners/test_cloud.py @@ -1,6 +1,4 @@ -""" -Cloud cleaner tests -""" +"""Cloud cleaner tests.""" import pytest @@ -8,8 +6,8 @@ @pytest.mark.parametrize( - "item,clean", - (("TSFEW004SCT012FEW///CBBKN080", "TS FEW004 SCT012 FEW///CB BKN080"),), + ("item", "clean"), + [("TSFEW004SCT012FEW///CBBKN080", "TS FEW004 SCT012 FEW///CB BKN080")], ) -def test_separate_cloud_layers(item: str, clean: str): +def test_separate_cloud_layers(item: str, clean: str) -> None: assert separate_cloud_layers(item) == clean diff --git a/tests/parsing/cleaners/test_joined.py b/tests/parsing/cleaners/test_joined.py index e4bc59a..e4f895c 100644 --- a/tests/parsing/cleaners/test_joined.py +++ b/tests/parsing/cleaners/test_joined.py @@ -1,6 +1,4 @@ -""" -Joined space cleaner tests -""" +"""Joined space cleaner tests.""" import pytest @@ -8,113 +6,113 @@ @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ ("OVC010FEW040", 6), ("SCT012CBOVC030TCU", 8), ("BKN01826/25", 6), ("BKN018M01/M05", 6), - ), + ], ) -def test_joined_cloud(item: str, index: str): +def test_joined_cloud(item: str, index: int) -> None: assert cleaners.JoinedCloud().split_at(item) == index @pytest.mark.parametrize( "item", - ("OVC012", "FEW100CB", "TX12/12/12"), + ["OVC012", "FEW100CB", "TX12/12/12"], ) -def test_not_joined_cloud(item: str): +def test_not_joined_cloud(item: str) -> None: assert cleaners.JoinedCloud().split_at(item) is None @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ ("123456Z36010KT", 7), ("161200Z10SM", 7), ("1210/12169999", 9), - ), + ], ) -def test_joined_timestamp(item: str, index: str): +def test_joined_timestamp(item: str, index: int) -> None: assert cleaners.JoinedTimestamp().split_at(item) == index @pytest.mark.parametrize( "item", - ("123456Z", "1210/1216", "TX12/12/12", "PROB30", "KTFM"), + ["123456Z", "1210/1216", "TX12/12/12", "PROB30", "KTFM"], ) -def test_not_joined_timestamp(item: str): +def test_not_joined_timestamp(item: str) -> None: assert cleaners.JoinedTimestamp().split_at(item) is None @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ # TODO: Add tests for non KT units ("36010KT10SM", 7), ("VRB10KTOVC010", 7), ("36010G15KT1210/1216", 10), - ), + ], ) -def test_joined_wind(item: str, index: str): +def test_joined_wind(item: str, index: int) -> None: assert cleaners.JoinedWind().split_at(item) == index @pytest.mark.parametrize( "item", - ("123456Z", "VRB10KT", "10SM"), + ["123456Z", "VRB10KT", "10SM"], ) -def test_not_joined_wind(item: str): +def test_not_joined_wind(item: str) -> None: assert cleaners.JoinedWind().split_at(item) is None @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ ("21016G28KTPROB40", 10), ("VCSHINTER", 4), - ), + ], ) -def test_joined_taf_new_line(item: str, index: str): +def test_joined_taf_new_line(item: str, index: int) -> None: assert cleaners.JoinedTafNewLine().split_at(item) == index @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ ("TX32/2521ZTN24/2512Z", 10), ("TN24/2512ZTX32/2521Z", 10), - ), + ], ) -def test_joined_min_max_temperature(item: str, index: str): +def test_joined_min_max_temperature(item: str, index: int) -> None: assert cleaners.JoinedMinMaxTemperature().split_at(item) == index @pytest.mark.parametrize( "item", - ("TX32/2521Z", "TXTN", "KMCO"), + ["TX32/2521Z", "TXTN", "KMCO"], ) -def test_not_joined_min_max_temperature(item: str): +def test_not_joined_min_max_temperature(item: str) -> None: assert cleaners.JoinedMinMaxTemperature().split_at(item) is None @pytest.mark.parametrize( - "item,index", - ( + ("item", "index"), + [ # TODO: Implement commented tests ("R36/1500DR18/P2000", 9), # ("R10C/2500FTTX32/2521Z", 11), # ("R04/0500KMCO", 8), - ), + ], ) -def test_joined_runway_visibility(item: str, index: str): +def test_joined_runway_visibility(item: str, index: int) -> None: assert cleaners.JoinedRunwayVisibility().split_at(item) == index @pytest.mark.parametrize( "item", - ("TX32/2521Z", "R01/1000FT", "KMCO"), + ["TX32/2521Z", "R01/1000FT", "KMCO"], ) -def test_not_joined_runway_visibility(item: str): +def test_not_joined_runway_visibility(item: str) -> None: assert cleaners.JoinedRunwayVisibility().split_at(item) is None diff --git a/tests/parsing/cleaners/test_remove.py b/tests/parsing/cleaners/test_remove.py index f9b7078..454255e 100644 --- a/tests/parsing/cleaners/test_remove.py +++ b/tests/parsing/cleaners/test_remove.py @@ -1,14 +1,12 @@ -""" -Item removal cleaner tests -""" +"""Item removal cleaner tests.""" import pytest -from avwx.parsing.sanitization.cleaners.base import RemoveItem from avwx.parsing.sanitization.cleaners import remove as cleaners +from avwx.parsing.sanitization.cleaners.base import RemoveItem -def test_remove_items_in(): +def test_remove_items_in() -> None: cleaner = cleaners.remove_items_in({"2", "4"})() assert isinstance(cleaner, RemoveItem) items = [str(i) for i in range(1, 6)] @@ -16,11 +14,11 @@ def test_remove_items_in(): assert filtered == ["1", "3", "5"] -@pytest.mark.parametrize("item", ("CCA", "CCB", "CCZ")) -def test_remove_taf_amend(item: str): +@pytest.mark.parametrize("item", ["CCA", "CCB", "CCZ"]) +def test_remove_taf_amend(item: str) -> None: assert cleaners.RemoveTafAmend().can_handle(item) is True -@pytest.mark.parametrize("item", ("CCTV", "CB")) -def test_not_remove_taf_amend(item: str): +@pytest.mark.parametrize("item", ["CCTV", "CB"]) +def test_not_remove_taf_amend(item: str) -> None: assert cleaners.RemoveTafAmend().can_handle(item) is False diff --git a/tests/parsing/cleaners/test_replace.py b/tests/parsing/cleaners/test_replace.py index 60b49e4..16b67a7 100644 --- a/tests/parsing/cleaners/test_replace.py +++ b/tests/parsing/cleaners/test_replace.py @@ -1,6 +1,4 @@ -""" -Item replacement cleaner tests -""" +"""Item replacement cleaner tests.""" import pytest @@ -8,19 +6,19 @@ @pytest.mark.parametrize( - "item,clean", - ( + ("item", "clean"), + [ ("CALM", "00000KT"), ("A01", "AO1"), ("PROB3O", "PROB30"), - ), + ], ) -def test_replace_item(item: str, clean: str): +def test_replace_item(item: str, clean: str) -> None: cleaner = cleaners.ReplaceItem() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("KMCO", "VRB12KT")) -def test_not_replace_item(item: str): +@pytest.mark.parametrize("item", ["KMCO", "VRB12KT"]) +def test_not_replace_item(item: str) -> None: assert cleaners.ReplaceItem().can_handle(item) is False diff --git a/tests/parsing/cleaners/test_separated.py b/tests/parsing/cleaners/test_separated.py index 47d5a64..9ef279a 100644 --- a/tests/parsing/cleaners/test_separated.py +++ b/tests/parsing/cleaners/test_separated.py @@ -1,6 +1,4 @@ -""" -Separated space cleaner tests -""" +"""Separated space cleaner tests.""" import pytest @@ -8,231 +6,225 @@ @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("10", "SM"), ("1", "0SM"), - ), + ], ) -def test_separated_distance(first: str, second: str): +def test_separated_distance(first: str, second: str) -> None: assert cleaners.SeparatedDistance().can_handle(first, second) is True -@pytest.mark.parametrize("first,second", (("KMCO", "10SM"),)) -def test_not_separated_distance(first: str, second: str): +@pytest.mark.parametrize(("first", "second"), [("KMCO", "10SM")]) +def test_not_separated_distance(first: str, second: str) -> None: assert cleaners.SeparatedDistance().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ # TODO: Add tests for M01 ("12", "/10"), ("01", "/00"), - ), + ], ) -def test_separated_first_temperature(first: str, second: str): +def test_separated_first_temperature(first: str, second: str) -> None: assert cleaners.SeparatedFirstTemperature().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("9999", "/Z"), ("KMCO", "/12"), - ), + ], ) -def test_not_separated_first_temperature(first: str, second: str): +def test_not_separated_first_temperature(first: str, second: str) -> None: assert cleaners.SeparatedFirstTemperature().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("OVC", "040"), ("FEW", "100"), - ), + ], ) -def test_separated_cloud_altitude(first: str, second: str): +def test_separated_cloud_altitude(first: str, second: str) -> None: assert cleaners.SeparatedCloudAltitude().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("OVC040", "9999"), ("FEW", "PLANES"), - ), + ], ) -def test_not_separated_cloud_altitude(first: str, second: str): +def test_not_separated_cloud_altitude(first: str, second: str) -> None: assert cleaners.SeparatedCloudAltitude().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ # TODO: Add tests for M01 ("12/", "10"), ("01/", "00"), - ), + ], ) -def test_separated_second_temperature(first: str, second: str): +def test_separated_second_temperature(first: str, second: str) -> None: assert cleaners.SeparatedSecondTemperature().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("TX/", "12"), ("TN/", "9999"), - ), + ], ) -def test_not_separated_second_temperature(first: str, second: str): +def test_not_separated_second_temperature(first: str, second: str) -> None: assert cleaners.SeparatedSecondTemperature().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("Q", "1001"), ("Q", "0998"), ("A", "2992"), ("A", "3011"), - ), + ], ) -def test_separated_altimeter_letter(first: str, second: str): +def test_separated_altimeter_letter(first: str, second: str) -> None: assert cleaners.SeparatedAltimeterLetter().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("Q", "9999"), ("A", "9999"), ("Z", "2992"), ("A", "B"), - ), + ], ) -def test_not_separated_altimeter_letter(first: str, second: str): +def test_not_separated_altimeter_letter(first: str, second: str) -> None: assert cleaners.SeparatedAltimeterLetter().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ # TODO: Add tests for M01 ("12/1", "0"), ("04/0", "1"), - ), + ], ) -def test_separated_temperature_trailing_digit(first: str, second: str): - assert ( - cleaners.SeparatedTemperatureTrailingDigit().can_handle(first, second) is True - ) +def test_separated_temperature_trailing_digit(first: str, second: str) -> None: + assert cleaners.SeparatedTemperatureTrailingDigit().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("12/1", "9999"), ("KMCO", "0"), - ), + ], ) -def test_not_separated_temperature_trailing_digit(first: str, second: str): - assert ( - cleaners.SeparatedTemperatureTrailingDigit().can_handle(first, second) is False - ) +def test_not_separated_temperature_trailing_digit(first: str, second: str) -> None: + assert cleaners.SeparatedTemperatureTrailingDigit().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ # TODO: Add tests for non KT units ("36010G20", "KT"), ("VRB04G15", "KT"), ("10320", "KT"), ("36010K", "T"), ("VRB11K", "T"), - ), + ], ) -def test_separated_wind_unit(first: str, second: str): +def test_separated_wind_unit(first: str, second: str) -> None: assert cleaners.SeparatedWindUnit().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("36005KT", "KT"), ("KMCO", "KT"), - ), + ], ) -def test_not_separated_wind_unit(first: str, second: str): +def test_not_separated_wind_unit(first: str, second: str) -> None: assert cleaners.SeparatedWindUnit().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("OVC022", "CB"), ("FEW040", "TCU"), - ), + ], ) -def test_separated_cloud_qualifier(first: str, second: str): +def test_separated_cloud_qualifier(first: str, second: str) -> None: assert cleaners.SeparatedCloudQualifier().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("OVC040", "OVC050"), ("36010KT", "SKC"), - ), + ], ) -def test_not_separated_cloud_qualifier(first: str, second: str): +def test_not_separated_cloud_qualifier(first: str, second: str) -> None: assert cleaners.SeparatedCloudQualifier().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("FM", "122400"), ("TL", "160400Z"), - ), + ], ) -def test_separated_taf_time_prefix(first: str, second: str): +def test_separated_taf_time_prefix(first: str, second: str) -> None: assert cleaners.SeparatedTafTimePrefix().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("FM", "10SM"), ("TL", "12/03"), - ), + ], ) -def test_not_separated_taf_time_prefix(first: str, second: str): +def test_not_separated_taf_time_prefix(first: str, second: str) -> None: assert cleaners.SeparatedTafTimePrefix().can_handle(first, second) is False @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("TX", "20/10"), ("TN", "M01/12"), - ), + ], ) -def test_separated_min_max_temperature_prefix(first: str, second: str): +def test_separated_min_max_temperature_prefix(first: str, second: str) -> None: assert cleaners.SeparatedMinMaxTemperaturePrefix().can_handle(first, second) is True @pytest.mark.parametrize( - "first,second", - ( + ("first", "second"), + [ ("TX", "9999"), ("KMCO", "20/10"), - ), + ], ) -def test_not_separated_min_max_temperature_prefix(first: str, second: str): - assert ( - cleaners.SeparatedMinMaxTemperaturePrefix().can_handle(first, second) is False - ) +def test_not_separated_min_max_temperature_prefix(first: str, second: str) -> None: + assert cleaners.SeparatedMinMaxTemperaturePrefix().can_handle(first, second) is False diff --git a/tests/parsing/cleaners/test_visibility.py b/tests/parsing/cleaners/test_visibility.py index faa1296..1992d8b 100644 --- a/tests/parsing/cleaners/test_visibility.py +++ b/tests/parsing/cleaners/test_visibility.py @@ -1,37 +1,35 @@ -""" -Visibility cleaner tests -""" +"""Visibility cleaner tests.""" import pytest from avwx.parsing.sanitization.cleaners import visibility as cleaners -@pytest.mark.parametrize("item", ("TP6SM", "6PSM")) -def test_visibility_greater_than(item: str): +@pytest.mark.parametrize("item", ["TP6SM", "6PSM"]) +def test_visibility_greater_than(item: str) -> None: cleaner = cleaners.VisibilityGreaterThan() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == "P6SM" -@pytest.mark.parametrize("item", ("6MPS")) -def test_not_visibility_greater_than(item: str): +@pytest.mark.parametrize("item", ["6MPS"]) +def test_not_visibility_greater_than(item: str) -> None: assert cleaners.VisibilityGreaterThan().can_handle(item) is False @pytest.mark.parametrize( - "item,clean", - ( + ("item", "clean"), + [ ("R02L/4000VP6000F", "R02L/4000VP6000FT"), ("R31C/1000F", "R31C/1000FT"), - ), + ], ) -def test_runway_visibility_unit(item: str, clean: str): +def test_runway_visibility_unit(item: str, clean: str) -> None: cleaner = cleaners.RunwayVisibilityUnit() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("R21/2000", "R01/1500M", "R01R/2000V4000FT")) -def test_not_runway_visibility_unit(item: str): +@pytest.mark.parametrize("item", ["R21/2000", "R01/1500M", "R01R/2000V4000FT"]) +def test_not_runway_visibility_unit(item: str) -> None: assert cleaners.RunwayVisibilityUnit().can_handle(item) is False diff --git a/tests/parsing/cleaners/test_wind.py b/tests/parsing/cleaners/test_wind.py index 0b6bf2c..dc16dad 100644 --- a/tests/parsing/cleaners/test_wind.py +++ b/tests/parsing/cleaners/test_wind.py @@ -1,6 +1,4 @@ -""" -Wind cleaner tests -""" +"""Wind cleaner tests.""" import pytest @@ -8,8 +6,8 @@ @pytest.mark.parametrize( - "wind,fixed", - ( + ("wind", "fixed"), + [ ("O6O/10G18K", "06010G18KT"), ("VRBL03KT", "VRB03KT"), ("12007G15SM", "12007G15KT"), @@ -19,84 +17,84 @@ ("33010G20KTKT", "33010G20KT"), ("VRB16G36KTKT", "VRB16G36KT"), ("WBB010", "VRB010"), - ), + ], ) -def test_sanitize_wind(wind: str, fixed: str): - """Tests the wind sanitization before checks""" +def test_sanitize_wind(wind: str, fixed: str) -> None: + """Test the wind sanitization before checks.""" assert cleaners.sanitize_wind(wind) == fixed @pytest.mark.parametrize("item", (f"{'/'*i}KT" for i in range(1, 8))) -def test_empty_wind(item: str): +def test_empty_wind(item: str) -> None: assert cleaners.EmptyWind().can_handle(item) is True -@pytest.mark.parametrize("item", ("/////", "KMCO", "//SM", "TX12/1234")) -def test_not_empty_wind(item: str): +@pytest.mark.parametrize("item", ["/////", "KMCO", "//SM", "TX12/1234"]) +def test_not_empty_wind(item: str) -> None: assert cleaners.EmptyWind().can_handle(item) is False -@pytest.mark.parametrize("item,clean", (("22022KTG40", "22022G40KT"),)) -def test_misplaced_wind_kt(item: str, clean: str): +@pytest.mark.parametrize(("item", "clean"), [("22022KTG40", "22022G40KT")]) +def test_misplaced_wind_kt(item: str, clean: str) -> None: cleaner = cleaners.MisplaceWindKT() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("KTEX")) -def test_not_misplaced_wind_kt(item: str): +@pytest.mark.parametrize("item", ["KTEX"]) +def test_not_misplaced_wind_kt(item: str) -> None: assert cleaners.MisplaceWindKT().can_handle(item) is False -@pytest.mark.parametrize("item,clean", (("360G17G32KT", "36017G32KT"),)) -def test_double_gust(item: str, clean: str): +@pytest.mark.parametrize(("item", "clean"), [("360G17G32KT", "36017G32KT")]) +def test_double_gust(item: str, clean: str) -> None: cleaner = cleaners.DoubleGust() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("KMCO", "22022G40KT", "VRB05KT")) -def test_not_double_gust(item: str): +@pytest.mark.parametrize("item", ["KMCO", "22022G40KT", "VRB05KT"]) +def test_not_double_gust(item: str) -> None: assert cleaners.DoubleGust().can_handle(item) is False @pytest.mark.parametrize( - "item,clean", - ( + ("item", "clean"), + [ ("ABC36017G32KT", "36017G32KT"), ("ABCVRB12KT", "VRB12KT"), - ), + ], ) -def test_wind_leading_mistype(item: str, clean: str): +def test_wind_leading_mistype(item: str, clean: str) -> None: cleaner = cleaners.WindLeadingMistype() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("36017G32KT", "WS020/07040", "WS020/07040KT")) -def test_not_wind_leading_mistype(item: str): +@pytest.mark.parametrize("item", ["36017G32KT", "WS020/07040", "WS020/07040KT"]) +def test_not_wind_leading_mistype(item: str) -> None: assert cleaners.WindLeadingMistype().can_handle(item) is False -@pytest.mark.parametrize("item,clean", (("14010-15KT", "14010G15KT"),)) -def test_non_g_gust(item: str, clean: str): +@pytest.mark.parametrize(("item", "clean"), [("14010-15KT", "14010G15KT")]) +def test_non_g_gust(item: str, clean: str) -> None: cleaner = cleaners.NonGGust() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("36017G32KT",)) -def test_not_non_g_gust(item: str): +@pytest.mark.parametrize("item", ["36017G32KT"]) +def test_not_non_g_gust(item: str) -> None: assert cleaners.NonGGust().can_handle(item) is False -@pytest.mark.parametrize("item,clean", (("2VRB02KT", "VRB02KT"),)) -def test_remove_vrb_leading_digits(item: str, clean: str): +@pytest.mark.parametrize(("item", "clean"), [("2VRB02KT", "VRB02KT")]) +def test_remove_vrb_leading_digits(item: str, clean: str) -> None: cleaner = cleaners.RemoveVrbLeadingDigits() assert cleaner.can_handle(item) is True assert cleaner.clean(item) == clean -@pytest.mark.parametrize("item", ("36017G32KT", "VRB12KT")) -def test_not_remove_vrb_leading_digits(item: str): +@pytest.mark.parametrize("item", ["36017G32KT", "VRB12KT"]) +def test_not_remove_vrb_leading_digits(item: str) -> None: assert cleaners.RemoveVrbLeadingDigits().can_handle(item) is False diff --git a/tests/parsing/test_core.py b/tests/parsing/test_core.py index c39e023..fc75226 100644 --- a/tests/parsing/test_core.py +++ b/tests/parsing/test_core.py @@ -1,12 +1,12 @@ -""" -Core Tests -""" +"""Core Tests.""" -# pylint: disable=too-many-public-methods,invalid-name +# ruff: noqa: FBT001,SLF001 # stdlib +from __future__ import annotations + from datetime import datetime, timezone -from typing import Any, List, Optional, Tuple, Union +from typing import Any # library import pytest @@ -22,67 +22,67 @@ @pytest.mark.parametrize( - "before,after,neighbors", - ( + ("before", "after", "neighbors"), + [ ([1, 2, 3, 2, 1], [1, 2, 3], False), ([4, 4, 4, 4], [4], False), ([1, 5, 1, 1, 3, 5], [1, 5, 3], False), ([1, 2, 3, 2, 1], [1, 2, 3, 2, 1], True), ([4, 4, 4, 4], [4], True), ([1, 5, 1, 1, 3, 5], [1, 5, 1, 3, 5], True), - ), + ], ) -def test_dedupe(before: List[int], after: List[int], neighbors: bool): - """Tests list deduplication""" +def test_dedupe(before: list[int], after: list[int], neighbors: bool) -> None: + """Test list deduplication.""" assert core.dedupe(before, only_neighbors=neighbors) == after -def test_is_unknown(): - """Tests unknown value when a string value contains only backspace characters or empty""" +def test_is_unknown() -> None: + """Test unknown value when a string value contains only backspace characters or empty.""" for i in range(10): assert core.is_unknown("/" * i) is True -@pytest.mark.parametrize("value", ("abc", "/bc", "a/c", "ab/", "a//", "/b/", "//c")) -def test_is_not_unknown(value: str): - """Tests full or partially known values""" +@pytest.mark.parametrize("value", ["abc", "/bc", "a/c", "ab/", "a//", "/b/", "//c"]) +def test_is_not_unknown(value: str) -> None: + """Test full or partially known values.""" assert core.is_unknown(value) is False -def test_bad_unknown(): +def test_bad_unknown() -> None: with pytest.raises(TypeError): - core.is_unknown(None) + core.is_unknown(None) # type: ignore -@pytest.mark.parametrize("ts", ("123456Z", "987654Z")) -def test_is_timestamp(ts: str): - """Tests determining if a string is a timestamp element""" +@pytest.mark.parametrize("ts", ["123456Z", "987654Z"]) +def test_is_timestamp(ts: str) -> None: + """Test determining if a string is a timestamp element.""" assert core.is_timestamp(ts) is True -@pytest.mark.parametrize("ts", ("", "123456Z123", "1234", "1234Z")) -def test_is_not_timestamp(ts: str): +@pytest.mark.parametrize("ts", ["", "123456Z123", "1234", "1234Z"]) +def test_is_not_timestamp(ts: str) -> None: assert core.is_timestamp(ts) is False @pytest.mark.parametrize( - "fraction,unpacked", - ( + ("fraction", "unpacked"), + [ ("", ""), ("1", "1"), ("1/2", "1/2"), ("3/2", "1 1/2"), ("10/3", "3 1/3"), - ), + ], ) -def test_unpack_fraction(fraction: str, unpacked: str): - """Tests unpacking a fraction where the numerator can be greater than the denominator""" +def test_unpack_fraction(fraction: str, unpacked: str) -> None: + """Test unpacking a fraction where the numerator can be greater than the denominator.""" assert core.unpack_fraction(fraction) == unpacked @pytest.mark.parametrize( - "num,stripped", - ( + ("num", "stripped"), + [ ("", ""), ("5", "5"), ("010", "10"), @@ -91,16 +91,16 @@ def test_unpack_fraction(fraction: str, unpacked: str): ("-09.9", "-9.9"), ("000", "0"), ("M00", "0"), - ), + ], ) -def test_remove_leading_zeros(num: str, stripped: str): - """Tests removing leading zeros from a number""" +def test_remove_leading_zeros(num: str, stripped: str) -> None: + """Test removing leading zeros from a number.""" assert core.remove_leading_zeros(num) == stripped @pytest.mark.parametrize( - "num,spoken", - ( + ("num", "spoken"), + [ ("1", "one"), ("5", "five"), ("20", "two zero"), @@ -109,16 +109,16 @@ def test_remove_leading_zeros(num: str, stripped: str): ("29.92", "two nine point nine two"), ("1/2", "one half"), ("3 3/4", "three and three quarters"), - ), + ], ) -def test_spoken_number(num: str, spoken: str): - """Tests converting digits into spoken values""" +def test_spoken_number(num: str, spoken: str) -> None: + """Test converting digits into spoken values.""" assert core.spoken_number(num) == spoken @pytest.mark.parametrize( - "num,value,spoken", - ( + ("num", "value", "spoken"), + [ ("1", 1, "one"), ("1.5", 1.5, "one point five"), ("060", 60, "six zero"), @@ -127,10 +127,10 @@ def test_spoken_number(num: str, spoken: str): ("M10", -10, "minus one zero"), ("FL310", 310, "flight level three one zero"), ("ABV FL480", 480, "above flight level four eight zero"), - ), + ], ) -def test_make_number(num: str, value: Union[int, float, None], spoken: str): - """Tests Number dataclass generation from a number string""" +def test_make_number(num: str, value: float | None, spoken: str) -> None: + """Test Number dataclass generation from a number string.""" number = core.make_number(num) assert isinstance(number, Number) assert number.repr == num @@ -139,14 +139,14 @@ def test_make_number(num: str, value: Union[int, float, None], spoken: str): @pytest.mark.parametrize( - "num,value,spoken", - ( + ("num", "value", "spoken"), + [ ("P6SM", None, "greater than six"), ("M1/4", None, "less than one quarter"), - ), + ], ) -def test_make_number_gt_lt(num: str, value: Union[int, float, None], spoken: str): - """Tests Number dataclass generation when using P/M for greater/less than""" +def test_make_number_gt_lt(num: str, value: float | None, spoken: str) -> None: + """Test Number dataclass generation when using P/M for greater/less than.""" number = core.make_number(num, m_minus=False) assert isinstance(number, Number) assert number.repr == num @@ -154,29 +154,29 @@ def test_make_number_gt_lt(num: str, value: Union[int, float, None], spoken: str assert number.spoken == spoken -def test_make_non_number(): +def test_make_non_number() -> None: assert core.make_number("") is None -def test_make_number_repr_override(): - assert core.make_number("1234", "A1234").repr == "A1234" +def test_make_number_repr_override() -> None: + num = core.make_number("1234", "A1234") + assert num is not None + assert num.repr == "A1234" @pytest.mark.parametrize( - "num,value,spoken,nmr,dnm,norm", - ( + ("num", "value", "spoken", "nmr", "dnm", "norm"), + [ ("1/4", 0.25, "one quarter", 1, 4, "1/4"), ("5/2", 2.5, "two and one half", 5, 2, "2 1/2"), ("2-1/2", 2.5, "two and one half", 5, 2, "2 1/2"), ("3/4", 0.75, "three quarters", 3, 4, "3/4"), ("5/4", 1.25, "one and one quarter", 5, 4, "1 1/4"), ("11/4", 1.25, "one and one quarter", 5, 4, "1 1/4"), - ), + ], ) -def test_make_number_fractions( - num: str, value: float, spoken: str, nmr: int, dnm: int, norm: str -): - """Tests Fraction dataclass generation from a number string""" +def test_make_number_fractions(num: str, value: float, spoken: str, nmr: int, dnm: int, norm: str) -> None: + """Test Fraction dataclass generation from a number string.""" number = core.make_number(num) assert isinstance(number, Fraction) assert number.value == value @@ -186,64 +186,68 @@ def test_make_number_fractions( assert number.normalized == norm -def test_make_number_speech(): - """Tests Number generation speech overrides""" +def test_make_number_speech() -> None: + """Test Number generation speak override.""" number = core.make_number("040", speak="040") + assert number is not None assert number.value == 40 assert number.spoken == "zero four zero" + + +def test_make_number_literal() -> None: + """Test Number generation literal override.""" number = core.make_number("100", literal=True) + assert number is not None assert number.value == 100 assert number.spoken == "one zero zero" @pytest.mark.parametrize( - "string,targets,index", - ( - ("012345", ("5", "2", "3"), 2), - ("This is weird", ("me", "you", "we"), 8), + ("string", "targets", "index"), + [ + ("012345", ["5", "2", "3"], 2), + ("This is weird", ["me", "you", "we"], 8), ("KJFK NOPE LOL RMK HAHAHA", static.metar.METAR_RMK, 13), - ), + ], ) -def test_find_first_in_list(string: str, targets: Tuple[str], index: int): - """Tests a function which finds the first occurrence in a string from a list +def test_find_first_in_list(string: str, targets: list[str], index: int) -> None: + """Test a function which finds the first occurrence in a string from a list. - This is used to find remarks and TAF time periods + This is used to find remarks and TAF time periods. """ assert core.find_first_in_list(string, targets) == index -@pytest.mark.parametrize("temp", ("10", "22", "333", "M05", "5")) -def test_is_possible_temp(temp: str): - """Tests if an element could be a formatted temperature""" +@pytest.mark.parametrize("temp", ["10", "22", "333", "M05", "5"]) +def test_is_possible_temp(temp: str) -> None: + """Test if an element could be a formatted temperature.""" assert core.is_possible_temp(temp) is True -@pytest.mark.parametrize("temp", ("A", "12.3", "MNA", "-13")) -def test_is_not_possible_temp(temp: str): +@pytest.mark.parametrize("temp", ["A", "12.3", "MNA", "-13"]) +def test_is_not_possible_temp(temp: str) -> None: assert core.is_possible_temp(temp) is False @pytest.mark.parametrize( - "wx,ret,station,time", - ( + ("wx", "ret", "station", "time"), + [ (["KJFK", "123456Z", "1"], ["1"], "KJFK", "123456Z"), (["KJFK", "123456", "1"], ["1"], "KJFK", "123456Z"), (["KJFK", "1234Z", "1"], ["1"], "KJFK", "1234Z"), (["KJFK", "1234", "1"], ["1234", "1"], "KJFK", None), (["KJFK", "1"], ["1"], "KJFK", None), (["KJFK"], [], "KJFK", None), - ), + ], ) -def test_get_station_and_time( - wx: List[str], ret: List[str], station: str, time: Optional[str] -): - """Tests removal of station (first item) and potential timestamp""" +def test_get_station_and_time(wx: list[str], ret: list[str], station: str, time: str | None) -> None: + """Test removal of station (first item) and potential timestamp.""" assert core.get_station_and_time(wx) == (ret, station, time) @pytest.mark.parametrize( - "wx,unit,wind,varv", - ( + ("wx", "unit", "wind", "varv"), + [ (["1"], "kt", ((None,), (None,), (None,)), []), (["12345", "G50", "1"], "kt", (("123", 123), ("45", 45), ("50", 50)), []), (["01234G56", "1"], "kt", (("012", 12), ("34", 34), ("56", 56)), []), @@ -259,10 +263,10 @@ def test_get_station_and_time( (["VRB20G30KMH", "1"], "km/h", (("VRB",), ("20", 20), ("30", 30)), []), (["03015G21MPH", "1"], "mi/h", (("030", 30), ("15", 15), ("21", 21)), []), (["16006GP99KT", "1"], "kt", (("160", 160), ("06", 6), ("P99", None)), []), - ), + ], ) -def test_get_wind(wx: List[str], unit: str, wind: Tuple[tuple], varv: List[tuple]): - """Tests that the wind item gets removed and split into its components""" +def test_get_wind(wx: list[str], unit: str, wind: tuple[tuple], varv: list[tuple]) -> None: + """Test that the wind item gets removed and split into its components.""" # Both use knots as the default unit, so just test North American default units = structs.Units.north_american() wx, *winds, var = core.get_wind(wx, units) @@ -277,8 +281,8 @@ def test_get_wind(wx: List[str], unit: str, wind: Tuple[tuple], varv: List[tuple @pytest.mark.parametrize( - "wx,unit,visibility", - ( + ("wx", "unit", "visibility"), + [ (["1"], "sm", (None,)), (["05SM", "1"], "sm", ("5", 5)), (["10SM", "1"], "sm", ("10", 10)), @@ -293,10 +297,10 @@ def test_get_wind(wx: List[str], unit: str, wind: Tuple[tuple], varv: List[tuple (["M1000", "1"], "m", ("1000", 1000)), (["2KM", "1"], "m", ("2000", 2000)), (["15KM", "1"], "m", ("15000", 15000)), - ), + ], ) -def test_get_visibility(wx: List[str], unit: str, visibility: tuple): - """Tests that the visibility item(s) gets removed and cleaned""" +def test_get_visibility(wx: list[str], unit: str, visibility: tuple) -> None: + """Test that the visibility item(s) gets removed and cleaned.""" units = structs.Units.north_american() wx, vis = core.get_visibility(wx, units) assert wx == ["1"] @@ -304,8 +308,8 @@ def test_get_visibility(wx: List[str], unit: str, visibility: tuple): assert_number(vis, *visibility) -def test_get_digit_list(): - """Tests that digits are removed after an index but before a non-digit item""" +def test_get_digit_list() -> None: + """Test that digits are removed after an index but before a non-digit item.""" items = ["1", "T", "2", "3", "ODD", "Q", "4", "C"] items, ret = core.get_digit_list(items, 1) assert items == ["1", "ODD", "Q", "4", "C"] @@ -316,8 +320,8 @@ def test_get_digit_list(): @pytest.mark.parametrize( - "bad,good", - ( + ("bad", "good"), + [ ("OVC", "OVC"), ("010", "010"), ("SCT060", "SCT060"), @@ -325,16 +329,16 @@ def test_get_digit_list(): ("BKNC015", "BKN015C"), ("FEW027///", "FEW027///"), ("UNKN021-TOP023", "UNKN021-TOP023"), - ), + ], ) -def test_sanitize_cloud(bad: str, good: str): - """Tests the common cloud issues are fixed before parsing""" +def test_sanitize_cloud(bad: str, good: str) -> None: + """Test the common cloud issues are fixed before parsing.""" assert core.sanitize_cloud(bad) == good @pytest.mark.parametrize( - "cloud,out", - ( + ("cloud", "out"), + [ ("SCT060", ["SCT", 60, None, None]), ("FEWO03", ["FEW", 3, None, None]), ("BKNC015", ["BKN", 15, None, "C"]), @@ -347,10 +351,10 @@ def test_sanitize_cloud(bad: str, good: str): ("OVC100-TOP110", ["OVC", 100, 110, None]), ("OVC065-TOPUNKN", ["OVC", 65, None, None]), ("SCT-BKN050-TOP100", ["SCT-BKN", 50, 100, None]), - ), + ], ) -def test_make_cloud(cloud: str, out: List[Any]): - """Tests helper function which returns a Cloud dataclass""" +def test_make_cloud(cloud: str, out: list[Any]) -> None: + """Test helper function which returns a Cloud dataclass.""" ret_cloud = core.make_cloud(cloud) assert isinstance(ret_cloud, structs.Cloud) assert ret_cloud.repr == cloud @@ -359,8 +363,8 @@ def test_make_cloud(cloud: str, out: List[Any]): @pytest.mark.parametrize( - "wx,clouds", - ( + ("wx", "clouds"), + [ (["1"], []), (["SCT060", "1"], [["SCT", 60, None]]), ( @@ -368,10 +372,10 @@ def test_make_cloud(cloud: str, out: List[Any]): [["VV", 10, None], ["SCT", 50, "C"], ["OVC", 100, None]], ), (["1", "BKN020", "SCT050"], [["BKN", 20, None], ["SCT", 50, None]]), - ), + ], ) -def test_get_clouds(wx: List[str], clouds: List[list]): - """Tests that clouds are removed, fixed, and split correctly""" +def test_get_clouds(wx: list[str], clouds: list[list]) -> None: + """Test that clouds are removed, fixed, and split correctly.""" wx, ret_clouds = core.get_clouds(wx) assert wx == ["1"] for i, cloud in enumerate(ret_clouds): @@ -381,8 +385,8 @@ def test_get_clouds(wx: List[str], clouds: List[list]): @pytest.mark.parametrize( - "vis,ceiling,rule", - ( + ("vis", "ceiling", "rule"), + [ (None, None, "IFR"), ("10", None, "VFR"), ("P6SM", ["OCV", 50], "VFR"), @@ -392,22 +396,21 @@ def test_get_clouds(wx: List[str], clouds: List[list]): ("6", ["OVC", 4], "LIFR"), ("1/2", ["OVC", 30], "LIFR"), ("M1/4", ["OVC", 30], "LIFR"), - ), + ], ) -def test_get_flight_rules(vis: Optional[str], ceiling: Optional[tuple], rule: str): - """Tests that the proper flight rule is calculated for a set visibility and ceiling +def test_get_flight_rules(vis: str | None, ceiling: tuple | None, rule: str) -> None: + """Test that the proper flight rule is calculated for a set visibility and ceiling. - Note: Only 'Broken', 'Overcast', and 'Vertical Visibility' are considered ceilings + Note: Only 'Broken', 'Overcast', and 'Vertical Visibility' are considered ceilings. """ - vis = core.make_number(vis, m_minus=False) - if ceiling: - ceiling = structs.Cloud(None, *ceiling) - assert static.core.FLIGHT_RULES[core.get_flight_rules(vis, ceiling)] == rule + vis_num = core.make_number(vis, m_minus=False) + cloud = structs.Cloud("", *ceiling) if ceiling else None + assert static.core.FLIGHT_RULES[core.get_flight_rules(vis_num, cloud)] == rule @pytest.mark.parametrize( - "clouds,ceiling", - ( + ("clouds", "ceiling"), + [ ([], None), ([["FEW", 10], ["SCT", 10]], None), ([["OVC", None]], None), @@ -415,32 +418,29 @@ def test_get_flight_rules(vis: Optional[str], ceiling: Optional[tuple], rule: st ([["OVC", 20], ["BKN", 30]], ["OVC", 20]), ([["OVC", None], ["BKN", 30]], ["BKN", 30]), ([["FEW", 10], ["OVC", 20]], ["OVC", 20]), - ), + ], ) -def test_get_ceiling(clouds: List[list], ceiling: list): - """Tests that the ceiling is properly identified from a list of clouds""" - clouds = [structs.Cloud(None, *cloud) for cloud in clouds] - if ceiling: - ceiling = structs.Cloud(None, *ceiling) - assert core.get_ceiling(clouds) == ceiling +def test_get_ceiling(clouds: list[list], ceiling: list) -> None: + """Test that the ceiling is properly identified from a list of clouds.""" + cloud_objs = [structs.Cloud("", *cloud) for cloud in clouds] + ceiling_obj = structs.Cloud("", *ceiling) if ceiling else None + assert core.get_ceiling(cloud_objs) == ceiling_obj -@pytest.mark.parametrize( - "altitude", ("SFC/FL030", "FL020/030", "6000FT/FL020", "300FT") -) -def test_is_altitude(altitude: str): - """Tests if an element is an altitude""" +@pytest.mark.parametrize("altitude", ["SFC/FL030", "FL020/030", "6000FT/FL020", "300FT"]) +def test_is_altitude(altitude: str) -> None: + """Test if an element is an altitude.""" assert core.is_altitude(altitude) is True -@pytest.mark.parametrize("value", ("", "50SE", "KFFT")) -def test_is_not_altitude(value: str): +@pytest.mark.parametrize("value", ["", "50SE", "KFFT"]) +def test_is_not_altitude(value: str) -> None: assert core.is_altitude(value) is False @pytest.mark.parametrize( - "text,force,value,unit,speak", - ( + ("text", "force", "value", "unit", "speak"), + [ ("FL030", False, 30, "ft", "flight level three zero"), ("030", False, 30, "ft", "three zero"), ("030", True, 30, "ft", "flight level three zero"), @@ -448,20 +448,21 @@ def test_is_not_altitude(value: str): ("10000FT", False, 10000, "ft", "one zero thousand"), ("2000M", False, 2000, "m", "two thousand"), ("ABV FL450", False, 450, "ft", "above flight level four five zero"), - ), + ], ) -def test_make_altitude(text: str, force: bool, value: int, unit: str, speak: str): - """Tests converting altitude text into Number""" +def test_make_altitude(text: str, force: bool, value: int, unit: str, speak: str) -> None: + """Test converting altitude text into Number.""" units = Units.international() altitude, units = core.make_altitude(text, units, force_fl=force) + assert altitude is not None assert altitude.repr == text assert units.altitude == unit assert altitude.value == value assert altitude.spoken == speak -def test_parse_date(): - """Tests that report timestamp is parsed into a datetime object""" +def test_parse_date() -> None: + """Test that report timestamp is parsed into a datetime object.""" today = datetime.now(tz=timezone.utc) rts = today.strftime(r"%d%H%MZ") parsed = core.parse_date(rts) @@ -472,8 +473,8 @@ def test_parse_date(): @time_machine.travel("2020-06-22 12:00") -def test_midnight_rollover(): - """Tests that hour > 23 gets rolled into the next day""" +def test_midnight_rollover() -> None: + """Test that hour > 23 gets rolled into the next day.""" parsed = core.parse_date("2224") assert isinstance(parsed, datetime) assert parsed.day == 23 @@ -482,71 +483,69 @@ def test_midnight_rollover(): @pytest.mark.parametrize( - "dt,fmt,target", - ( + ("dt", "fmt", "target"), + [ (datetime.now(tz=timezone.utc), r"%d%HZ", False), (datetime.now(tz=timezone.utc), r"%d%H%MZ", False), (datetime(2010, 2, 2, 2, 2, tzinfo=timezone.utc), r"%d%HZ", True), (datetime(2010, 2, 2, 2, 2, tzinfo=timezone.utc), r"%d%H%MZ", True), - ), + ], ) -def test_make_timestamp(dt: datetime, fmt: str, target: bool): - """Tests that a report timestamp is converted into a Timestamp dataclass""" +def test_make_timestamp(dt: datetime, fmt: str, target: bool) -> None: + """Test that a report timestamp is converted into a Timestamp dataclass.""" dt_repr = dt.strftime(fmt) - target = dt.date() if target else None + target_date = dt.date() if target else None dt = dt.replace(second=0, microsecond=0) if "%M" not in fmt: dt = dt.replace(minute=0) - ts = core.make_timestamp(dt_repr, target_date=target) + ts = core.make_timestamp(dt_repr, target_date=target_date) assert_timestamp(ts, dt_repr, dt) @pytest.mark.parametrize( - "temperature,dewpoint,humidity", - ( + ("temperature", "dewpoint", "humidity"), + [ (10, 5, 0.7107), (27, 24, 0.83662), (15, 0, 0.35868), (10, 10, 1.0), - ), + ], ) -def test_relative_humidity(temperature: int, dewpoint: int, humidity: float): - """Tests calculating relative humidity from temperatrue and dewpoint""" +def test_relative_humidity(temperature: int, dewpoint: int, humidity: float) -> None: + """Test calculating relative humidity from temperatrue and dewpoint.""" value = core.relative_humidity(temperature, dewpoint) assert round(value, 5) == humidity @pytest.mark.parametrize( - "pressure,altitude,pressure_altitude", - ( + ("pressure", "altitude", "pressure_altitude"), + [ (29.92, 0, 0), (30.12, 6400, 6200), (30.28, 12000, 11640), (29.78, 1200, 1340), (30.09, 0, -170), - ), + ], ) -def test_pressure_altitude(pressure: float, altitude: int, pressure_altitude: int): - """Tests calculating pressure altitude in feet""" +def test_pressure_altitude(pressure: float, altitude: int, pressure_altitude: int) -> None: + """Test calculating pressure altitude in feet.""" value = core.pressure_altitude(pressure, altitude) assert value == pressure_altitude @pytest.mark.parametrize( - "pressure,temperature,altitude,density", - ( + ("pressure", "temperature", "altitude", "density"), + [ (29.92, 15, 0, 0), (30.12, 10, 6400, 7136), (30.28, -10, 12000, 11520), (29.78, 18, 1200, 1988), (30.09, 31, 0, 1750), (30.02, 0, 0, -1900), - ), + ], ) -def test_density_altitude( - pressure: float, temperature: int, altitude: int, density: int -): - """Tests calculating density altitude in feet""" +def test_density_altitude(pressure: float, temperature: int, altitude: int, density: int) -> None: + """Test calculating density altitude in feet.""" units = Units.north_american() value = core.density_altitude(pressure, temperature, altitude, units) assert value == density diff --git a/tests/parsing/test_remarks.py b/tests/parsing/test_remarks.py index 9ebe1a5..fdb7f5c 100644 --- a/tests/parsing/test_remarks.py +++ b/tests/parsing/test_remarks.py @@ -1,9 +1,7 @@ -""" -Tests remarks elements parsing -""" +"""Tests remarks elements parsing.""" # stdlib -from typing import List, Optional +from __future__ import annotations # library import pytest @@ -16,26 +14,28 @@ from tests.util import assert_value -@pytest.mark.parametrize("code,temp", (("1045", -4.5), ("0237", 23.7), ("0987", 98.7))) -def test_decimal_code(code: str, temp: Optional[float]): - """Tests that a 4-digit number gets decoded into a temperature number""" - assert remarks.decimal_code(code).value == temp +@pytest.mark.parametrize(("code", "temp"), [("1045", -4.5), ("0237", 23.7), ("0987", 98.7)]) +def test_decimal_code(code: str, temp: float | None) -> None: + """Test that a 4-digit number gets decoded into a temperature number.""" + num = remarks.decimal_code(code) + assert isinstance(num, structs.Number) + assert num.value == temp -def test_bad_decimal_code(): - """Tests empty code value""" +def test_bad_decimal_code() -> None: + """Test empty code value.""" assert remarks.decimal_code("") is None @pytest.mark.parametrize( - "codes,temperature,dewpoint", - ( + ("codes", "temperature", "dewpoint"), + [ (["123"], None, None), (["123", "T01231234"], 12.3, -23.4), - ), + ], ) -def test_temp_dew_decimal(codes: List[str], temperature: float, dewpoint: float): - """Tests extracting temperature and dewpoint decimal values""" +def test_temp_dew_decimal(codes: list[str], temperature: float, dewpoint: float) -> None: + """Test extracting temperature and dewpoint decimal values.""" codes, temp, dew = remarks.temp_dew_decimal(codes) assert codes == ["123"] assert_value(temp, temperature) @@ -43,14 +43,14 @@ def test_temp_dew_decimal(codes: List[str], temperature: float, dewpoint: float) @pytest.mark.parametrize( - "codes,maximum,minimum", - ( + ("codes", "maximum", "minimum"), + [ (["123"], None, None), (["123", "401231432"], 12.3, -43.2), - ), + ], ) -def test_temp_minmax(codes: List[str], maximum: float, minimum: float): - """Tests extracting 24-hour min max temperatures""" +def test_temp_minmax(codes: list[str], maximum: float, minimum: float) -> None: + """Test extracting 24-hour min max temperatures.""" codes, tmax, tmin = remarks.temp_minmax(codes) assert codes == ["123"] assert_value(tmax, maximum) @@ -58,18 +58,16 @@ def test_temp_minmax(codes: List[str], maximum: float, minimum: float): @pytest.mark.parametrize( - "codes,precipitation,snowlevel", - ( + ("codes", "precipitation", "snowlevel"), + [ (["123"], None, None), (["123", "P1234"], 12.34, None), (["123", "4/123"], None, 123), (["123", "P0987", "4/098"], 9.87, 98), - ), + ], ) -def test_precip_snow( - codes: List[str], precipitation: Optional[float], snowlevel: Optional[int] -): - """Tests extracting hourly precipitation and snow accumulation""" +def test_precip_snow(codes: list[str], precipitation: float | None, snowlevel: int | None) -> None: + """Test extracting hourly precipitation and snow accumulation.""" codes, precip, snow = remarks.precip_snow(codes) assert codes == ["123"] assert_value(precip, precipitation) @@ -77,21 +75,21 @@ def test_precip_snow( @pytest.mark.parametrize( - "codes,pressure", - ( + ("codes", "pressure"), + [ (["123"], None), (["123", "SLP988"], 998.8), (["123", "SLP241"], 1024.1), - ), + ], ) -def test_sea_level_pressure(codes: List[str], pressure: float): - """Tests extracting the inferred sea level pressure in mb""" +def test_sea_level_pressure(codes: list[str], pressure: float) -> None: + """Test extracting the inferred sea level pressure in mb.""" codes, sea = remarks.sea_level_pressure(codes) assert codes == ["123"] assert_value(sea, pressure) -def test_no_sea_level_pressure(): +def test_no_sea_level_pressure() -> None: codes = ["123", "SLPNO"] new_codes, sea = remarks.sea_level_pressure(codes) assert codes == new_codes @@ -99,40 +97,37 @@ def test_no_sea_level_pressure(): @pytest.mark.parametrize( - "code,value,text", - ( + ("code", "value", "text"), + [ ("51234", 23.4, "Increasing, then steady"), ("54567", 56.7, "Steady"), - ), + ], ) -def test_parse_pressure(code: str, value: float, text: str): - """Tests parsing a 5-digit code to PressureTendency""" +def test_parse_pressure(code: str, value: float, text: str) -> None: + """Test parsing a 5-digit code to PressureTendency.""" pressure = remarks.parse_pressure(code) assert pressure == structs.PressureTendency(code, text, value) @pytest.mark.parametrize( - "code,value", - ( + ("code", "value"), + [ ("61234", 12.34), ("70123", 1.23), ("70012", 0.12), - ), + ], ) -def test_parse_precipitation(code: str, value: float): - """Tests parsing a 5-digit precipitation amount code""" +def test_parse_precipitation(code: str, value: float) -> None: + """Test parsing a 5-digit precipitation amount code.""" assert_value(remarks.parse_precipitation(code), value) -def test_parse(): - """Tests full remarks parsing""" +def test_parse() -> None: + """Test full remarks parsing.""" assert remarks.parse("") is None - rmk = ( - "RMK 10123 21234 51234 61234 74321 98321 " - "T01231234 403451567 P1234 4/123 SLP998 " - "ACFT MSHP TSB20 AO2 $" - ) + rmk = "RMK 10123 21234 51234 61234 74321 98321 " "T01231234 403451567 P1234 4/123 SLP998 " "ACFT MSHP TSB20 AO2 $" data = remarks.parse(rmk) + assert data is not None # 5-digit codes assert_value(data.maximum_temperature_6, 12.3) assert_value(data.minimum_temperature_6, -23.4) diff --git a/tests/parsing/test_sanitization.py b/tests/parsing/test_sanitization.py index 6fbef0f..7d56b85 100644 --- a/tests/parsing/test_sanitization.py +++ b/tests/parsing/test_sanitization.py @@ -1,6 +1,4 @@ -""" -Report Sanitization Tests -""" +"""Report Sanitization Tests.""" # stdlib import json @@ -20,12 +18,10 @@ TAF_CASES = DATA_PATH / "sanitize_taf_list_cases.json" -def test_sanitize_report_string(): - """Tests a function which fixes common mistakes while the report is a string""" +def test_sanitize_report_string() -> None: + """Test a function which fixes common mistakes while the report is a string.""" line = "KJFK 36010 ? TSFEW004SCT012FEW///CBBKN080 R02L/4000VP6000F C A V O K A2992" - fixed = ( - "KJFK 36010 TS FEW004 SCT012 FEW///CB BKN080 R02L/4000VP6000FT CAVOK A2992" - ) + fixed = "KJFK 36010 TS FEW004 SCT012 FEW///CB BKN080 R02L/4000VP6000FT CAVOK A2992" sans = Sanitization() assert clean_metar_string(line, sans) == fixed assert sans.errors_found is True @@ -36,13 +32,13 @@ def test_sanitize_report_string(): assert sans.replaced == {"C A V O K": "CAVOK", "P6000F": "P6000FT"} -def test_sanitize_empty_report_string(): - """Tests that the sanitization minimaly affects short text""" +def test_sanitize_empty_report_string() -> None: + """Test that the sanitization minimaly affects short text.""" assert clean_metar_string(" MVP=", Sanitization()) == "MVP" -def _test_list_sanitizer(cleaner: Callable, case: dict): - """Tests a function which fixes common mistakes while the report is a list""" +def _test_list_sanitizer(cleaner: Callable, case: dict) -> None: + """Test a function which fixes common mistakes while the report is a list.""" line, fixed = case["report"].split(), case["fixed"].split() sans = Sanitization() data = cleaner(line, sans) @@ -56,10 +52,10 @@ def _test_list_sanitizer(cleaner: Callable, case: dict): @pytest.mark.parametrize("case", json.load(METAR_CASES.open())) -def test_clean_metar_list(case: dict): +def test_clean_metar_list(case: dict) -> None: _test_list_sanitizer(clean_metar_list, case) @pytest.mark.parametrize("case", json.load(TAF_CASES.open())) -def test_clean_taf_list(case: dict): +def test_clean_taf_list(case: dict) -> None: _test_list_sanitizer(clean_taf_list, case) diff --git a/tests/parsing/test_speech.py b/tests/parsing/test_speech.py index e39d579..a8494b6 100644 --- a/tests/parsing/test_speech.py +++ b/tests/parsing/test_speech.py @@ -1,11 +1,7 @@ -""" -Tests speech parsing -""" - -# pylint: disable=redefined-builtin +"""Tests speech parsing.""" # stdlib -from typing import List, Optional, Tuple +from __future__ import annotations # library import pytest @@ -18,8 +14,8 @@ @pytest.mark.parametrize( - "wind,vardir,spoken", - ( + ("wind", "vardir", "spoken"), + [ (("", "", ""), None, "unknown"), ( ("360", "12", "20"), @@ -33,34 +29,33 @@ ["240", "300"], "two seven zero (variable two four zero to three zero zero) at 10 knots", ), - ), + ], ) -def test_wind(wind: Tuple[str, str, str], vardir: Optional[List[str]], spoken: str): - """Tests converting wind data into a spoken string""" - wind = [core.make_number(v, literal=(not i)) for i, v in enumerate(wind)] - if vardir: - vardir = [core.make_number(i, speak=i, literal=True) for i in vardir] - assert speech.wind(*wind, vardir) == f"Winds {spoken}" +def test_wind(wind: tuple[str, str, str], vardir: list[str] | None, spoken: str) -> None: + """Test converting wind data into a spoken string.""" + wind_nums = [core.make_number(v, literal=(not i)) for i, v in enumerate(wind)] + vardir_nums = [core.make_number(i, speak=i, literal=True) for i in vardir] if vardir else None + assert speech.wind(*wind_nums, vardir_nums) == f"Winds {spoken}" # type: ignore @pytest.mark.parametrize( - "temp,unit,spoken", - ( + ("temp", "unit", "spoken"), + [ ("", "F", "unknown"), ("20", "F", "two zero degrees Fahrenheit"), ("M20", "F", "minus two zero degrees Fahrenheit"), ("20", "C", "two zero degrees Celsius"), ("1", "C", "one degree Celsius"), - ), + ], ) -def test_temperature(temp: str, unit: str, spoken: str): - """Tests converting a temperature into a spoken string""" +def test_temperature(temp: str, unit: str, spoken: str) -> None: + """Test converting a temperature into a spoken string.""" assert speech.temperature("Temp", core.make_number(temp), unit) == f"Temp {spoken}" @pytest.mark.parametrize( - "vis,unit,spoken", - ( + ("vis", "unit", "spoken"), + [ ("", "m", "unknown"), ("0000", "m", "zero kilometers"), ("2000", "m", "two kilometers"), @@ -70,19 +65,16 @@ def test_temperature(temp: str, unit: str, spoken: str): ("3/4", "sm", "three quarters of a mile"), ("3/2", "sm", "one and one half miles"), ("3", "sm", "three miles"), - ), + ], ) -def test_visibility(vis: str, unit: str, spoken: str): - """Tests converting visibility distance into a spoken string""" - assert ( - speech.visibility(core.make_number(vis, m_minus=False), unit) - == f"Visibility {spoken}" - ) +def test_visibility(vis: str, unit: str, spoken: str) -> None: + """Test converting visibility distance into a spoken string.""" + assert speech.visibility(core.make_number(vis, m_minus=False), unit) == f"Visibility {spoken}" @pytest.mark.parametrize( - "alt,unit,spoken", - ( + ("alt", "unit", "spoken"), + [ ("", "hPa", "unknown"), ("1020", "hPa", "one zero two zero"), ("0999", "hPa", "zero nine nine nine"), @@ -90,16 +82,16 @@ def test_visibility(vis: str, unit: str, spoken: str): ("3000", "inHg", "three zero point zero zero"), ("2992", "inHg", "two nine point nine two"), ("3005", "inHg", "three zero point zero five"), - ), + ], ) -def test_altimeter(alt: str, unit: str, spoken: str): - """Tests converting altimeter reading into a spoken string""" +def test_altimeter(alt: str, unit: str, spoken: str) -> None: + """Test converting altimeter reading into a spoken string.""" assert speech.altimeter(parse_altimeter(alt), unit) == f"Altimeter {spoken}" @pytest.mark.parametrize( - "codes,spoken", - ( + ("codes", "spoken"), + [ ([], ""), ( ["+RATS", "VCFC"], @@ -109,16 +101,16 @@ def test_altimeter(alt: str, unit: str, spoken: str): ["-GR", "FZFG", "BCBLSN"], "Light Hail. Freezing Fog. Patchy Blowing Snow", ), - ), + ], ) -def test_wx_codes(codes: List[str], spoken: str): - """Tests converting WX codes into a spoken string""" - codes = get_wx_codes(codes)[1] - assert speech.wx_codes(codes) == spoken +def test_wx_codes(codes: list[str], spoken: str) -> None: + """Test converting WX codes into a spoken string.""" + wx_codes = get_wx_codes(codes)[1] + assert speech.wx_codes(wx_codes) == spoken -def test_metar(): # sourcery skip: dict-assign-update-to-union - """Tests converting METAR data into into a single spoken string""" +def test_metar() -> None: + """Test converting METAR data into into a single spoken string.""" units = structs.Units.north_american() empty_fields = ( "raw", @@ -146,9 +138,8 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union core.make_number("020", speak="020"), ], "wx_codes": get_wx_codes(["+RA"])[1], - } - data.update({k: None for k in empty_fields}) - data = structs.MetarData(**data) + } | {k: None for k in empty_fields} + metar_data = structs.MetarData(**data) # type: ignore spoken = ( "Winds three six zero (variable three four zero to zero two zero) " "at 12 knots gusting to 20 knots. Visibility three miles. " @@ -156,14 +147,14 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union "Temperature three degrees Celsius. Dew point minus one degree Celsius. " "Altimeter two nine point nine two" ) - ret = speech.metar(data, units) + ret = speech.metar(metar_data, units) assert isinstance(ret, str) assert ret == spoken @pytest.mark.parametrize( - "type,start,end,prob,spoken", - ( + ("type", "start", "end", "prob", "spoken"), + [ (None, None, None, None, ""), ("FROM", "2808", "2815", None, "From 8 to 15 zulu,"), ("FROM", "2822", "2903", None, "From 22 to 3 zulu,"), @@ -184,39 +175,38 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union ), ("INTER", "2423", "2500", None, "From 23 to midnight zulu, intermittent"), ("TEMPO", "0102", "0103", None, "From 2 to 3 zulu, temporary"), - ), + ], ) def test_type_and_times( - type: Optional[str], - start: Optional[str], - end: Optional[str], - prob: Optional[str], + type: str | None, # noqa: A002 + start: str | None, + end: str | None, + prob: str | None, spoken: str, -): - """Tests line start from type, time, and probability values""" +) -> None: + """Test line start from type, time, and probability values.""" start_ts, end_ts = core.make_timestamp(start), core.make_timestamp(end) - if prob is not None: - prob = core.make_number(prob) - ret = speech.type_and_times(type, start_ts, end_ts, prob) + prob_num = core.make_number(prob) if prob is not None else None + ret = speech.type_and_times(type, start_ts, end_ts, prob_num) assert isinstance(ret, str) assert ret == spoken @pytest.mark.parametrize( - "shear,spoken", - ( + ("shear", "spoken"), + [ ("", "Wind shear unknown"), ("WS020/07040KT", "Wind shear 2000ft from zero seven zero at 40 knots"), ("WS100/20020KT", "Wind shear 10000ft from two zero zero at 20 knots"), - ), + ], ) -def test_wind_shear(shear: str, spoken: str): - """Tests converting wind shear code into a spoken string""" +def test_wind_shear(shear: str, spoken: str) -> None: + """Test converting wind shear code into a spoken string.""" assert speech.wind_shear(shear) == spoken -def test_taf_line(): # sourcery skip: dict-assign-update-to-union - """Tests converting TAF line data into into a single spoken string""" +def test_taf_line() -> None: + """Test converting TAF line data into into a single spoken string.""" units = structs.Units.north_american() empty_fields = ("flight_rules", "probability", "raw", "sanitized") line = { @@ -236,9 +226,8 @@ def test_taf_line(): # sourcery skip: dict-assign-update-to-union "wind_speed": core.make_number("12"), "wx_codes": get_wx_codes(["+RA"])[1], "wind_variable_direction": [core.make_number("320"), core.make_number("370")], - } - line.update({k: None for k in empty_fields}) - line = structs.TafLineData(**line) + } | {k: None for k in empty_fields} + line_data = structs.TafLineData(**line) # type: ignore spoken = ( "From 2 to 6 zulu, Winds three six zero (variable three two zero to three seven zero) at 12 knots gusting to 20 knots. " "Wind shear 2000ft from zero seven zero at 40 knots. Visibility three miles. " @@ -247,18 +236,17 @@ def test_taf_line(): # sourcery skip: dict-assign-update-to-union "Occasional moderate turbulence in clouds from 5500ft to 8500ft. " "Light icing from 10000ft to 15000ft" ) - ret = speech.taf_line(line, units) + ret = speech.taf_line(line_data, units) assert isinstance(ret, str) assert ret == spoken -def test_taf(): - """Tests converting a TafData report into a single spoken string""" +def test_taf() -> None: + """Test converting a TafData report into a single spoken string.""" units = structs.Units.north_american() - # pylint: disable=no-member - empty_line = {k: None for k in structs.TafLineData.__dataclass_fields__.keys()} + empty_line = {k: None for k in structs.TafLineData.__dataclass_fields__} forecast = [ - structs.TafLineData(**{**empty_line, **line}) + structs.TafLineData(**{**empty_line, **line}) # type: ignore for line in ( { "type": "FROM", @@ -279,8 +267,8 @@ def test_taf(): ) ] taf = structs.TafData( - raw=None, - sanitized=None, + raw="", + sanitized="", remarks=None, station=None, time=None, @@ -289,6 +277,8 @@ def test_taf(): end_time=core.make_timestamp("0414Z"), ) ret = speech.taf(taf, units) + assert taf.start_time is not None + assert taf.start_time.dt is not None spoken = ( f"Starting on {taf.start_time.dt.strftime('%B')} 4th - From 10 to 14 zulu, " "Winds three six zero at 12 knots gusting to 20 knots. Visibility three miles. " diff --git a/tests/parsing/test_summary.py b/tests/parsing/test_summary.py index bd6487f..16fcf69 100644 --- a/tests/parsing/test_summary.py +++ b/tests/parsing/test_summary.py @@ -1,14 +1,12 @@ -""" -Test summary functions -""" +"""Test summary functions.""" # module from avwx import structs from avwx.parsing import summary -def test_metar(): - """Tests that METAR translations are summarized in the proper order""" +def test_metar() -> None: + """Test that METAR translations are summarized in the proper order.""" trans = structs.MetarTrans( altimeter="29.92 inHg (1013 hPa)", clouds="Broken layer at 1500ft (Cumulonimbus) - Reported AGL", @@ -27,8 +25,8 @@ def test_metar(): assert summary.metar(trans) == text -def test_taf(): - """Tests that TAF line translations are summarized in the proper order""" +def test_taf() -> None: + """Test that TAF line translations are summarized in the proper order.""" trans = structs.TafLineTrans( altimeter="29.92 inHg (1013 hPa)", clouds="Broken layer at 1500ft (Cumulonimbus) - Reported AGL", diff --git a/tests/parsing/test_translate.py b/tests/parsing/test_translate.py index 1aeb8e0..9814bae 100644 --- a/tests/parsing/test_translate.py +++ b/tests/parsing/test_translate.py @@ -1,11 +1,7 @@ -""" -Test translation functions -""" - -# pytest: disable=redefined-builtin +"""Test translation functions.""" # stdlib -from typing import Dict, List, Optional, Tuple +from __future__ import annotations # library import pytest @@ -17,8 +13,8 @@ @pytest.mark.parametrize( - "vis,unit,translation", - ( + ("vis", "unit", "translation"), + [ ("", "m", ""), ("0000", "m", "0km (0sm)"), ("2000", "m", "2km (1.2sm)"), @@ -28,19 +24,16 @@ ("3/4", "sm", "0.75sm (1.2km)"), ("3/2", "sm", "1.5sm (2.4km)"), ("3", "sm", "3sm (4.8km)"), - ), + ], ) -def test_visibility(vis: str, unit: str, translation: str): - """Tests visibility translation and conversion""" - assert ( - translate.base.visibility(core.make_number(vis, m_minus=False), unit) - == translation - ) +def test_visibility(vis: str, unit: str, translation: str) -> None: + """Test visibility translation and conversion.""" + assert translate.base.visibility(core.make_number(vis, m_minus=False), unit) == translation @pytest.mark.parametrize( - "alt,repr,unit,translation", - ( + ("alt", "repr", "unit", "translation"), + [ ("", "", "hPa", ""), ("1020", "1020", "hPa", "1020 hPa (30.12 inHg)"), ("0999", "0999", "hPa", "999 hPa (29.50 inHg)"), @@ -48,51 +41,51 @@ def test_visibility(vis: str, unit: str, translation: str): ("30.00", "3000", "inHg", "30.00 inHg (1016 hPa)"), ("29.92", "2992", "inHg", "29.92 inHg (1013 hPa)"), ("30.05", "3005", "inHg", "30.05 inHg (1018 hPa)"), - ), + ], ) -def test_altimeter(alt: str, repr: str, unit: str, translation: str): - """Tests altimeter translation and conversion""" +def test_altimeter(alt: str, repr: str, unit: str, translation: str) -> None: # noqa: A002 + """Test altimeter translation and conversion.""" assert translate.base.altimeter(core.make_number(alt, repr), unit) == translation @pytest.mark.parametrize( - "clouds,translation", - ( + ("clouds", "translation"), + [ (["BKN", "FEW020"], "Few clouds at 2000ft"), ( ["OVC030", "SCT100"], "Overcast layer at 3000ft, Scattered clouds at 10000ft", ), (["BKN015CB"], "Broken layer at 1500ft (Cumulonimbus)"), - ), + ], ) -def test_clouds(clouds: List[str], translation: str): - """Tests translating each cloud into a single string""" - clouds = [core.make_cloud(cloud) for cloud in clouds] - assert translate.base.clouds(clouds) == f"{translation} - Reported AGL" +def test_clouds(clouds: list[str], translation: str) -> None: + """Test translating each cloud into a single string.""" + cloud_objs = [core.make_cloud(cloud) for cloud in clouds] + assert translate.base.clouds(cloud_objs) == f"{translation} - Reported AGL" -def test_no_clouds(): +def test_no_clouds() -> None: assert translate.base.clouds(None) == "" assert translate.base.clouds([]) == "Sky clear" @pytest.mark.parametrize( - "codes,translation", - ( + ("codes", "translation"), + [ ([], ""), (["VCFC", "+RA"], "Vicinity Funnel Cloud, Heavy Rain"), (["-SN"], "Light Snow"), - ), + ], ) -def test_wx_codes(codes: List[str], translation: str): - """Tests translating a list of weather codes into a single string""" - codes = get_wx_codes(codes)[1] - assert translate.base.wx_codes(codes) == translation +def test_wx_codes(codes: list[str], translation: str) -> None: + """Test translating a list of weather codes into a single string.""" + code_objs = get_wx_codes(codes)[1] + assert translate.base.wx_codes(code_objs) == translation -def test_shared(): - """Tests availability of shared values between the METAR and TAF translations""" +def test_shared() -> None: + """Test availability of shared values between the METAR and TAF translations.""" units = structs.Units.north_american() data = structs.SharedData( altimeter=core.make_number("29.92"), @@ -114,17 +107,13 @@ def test_shared(): # Test METAR translations -def test_cardinal_direction(): - """Tests that a direction int returns the correct cardinal direction string""" +def test_cardinal_direction() -> None: + """Test that a direction int returns the correct cardinal direction string.""" # 12 - 360+ keys = (12, 34, 57, 79) for i, cardinal in enumerate(static.core.CARDINAL_DEGREES.keys()): lower = keys[i % 4] + 90 * (i // 4) - upper = ( - keys[0] + 90 * ((i // 4) + 1) - 1 - if i % 4 == 3 - else keys[(i % 4) + 1] + 90 * (i // 4) - 1 - ) + upper = keys[0] + 90 * ((i // 4) + 1) - 1 if i % 4 == 3 else keys[(i % 4) + 1] + 90 * (i // 4) - 1 for direction in range(lower, upper + 1): assert translate.base.get_cardinal_direction(direction) == cardinal # -10 - 11 @@ -133,8 +122,8 @@ def test_cardinal_direction(): @pytest.mark.parametrize( - "wind,vardir,translation", - ( + ("wind", "vardir", "translation"), + [ (("", "", ""), None, ""), ( ("360", "12", "20"), @@ -144,35 +133,32 @@ def test_cardinal_direction(): (("000", "00", ""), None, "Calm"), (("VRB", "5", "12"), None, "Variable at 5kt gusting to 12kt"), (("270", "10", ""), ["240", "300"], "W-270 (variable 240 to 300) at 10kt"), - ), + ], ) -def test_wind( - wind: Tuple[str, str, str], vardir: Optional[List[str]], translation: str -): - """Tests that wind values are translating into a single string""" - wind = [core.make_number(i) for i in wind] - if vardir: - vardir = [core.make_number(i) for i in vardir] - assert translate.base.wind(*wind, vardir) == translation +def test_wind(wind: tuple[str, str, str], vardir: list[str] | None, translation: str) -> None: + """Test that wind values are translating into a single string.""" + wind_nums = [core.make_number(i) for i in wind] + vardir_nums = [core.make_number(i) for i in vardir] if vardir else None + assert translate.base.wind(*wind_nums, vardir_nums) == translation # type: ignore @pytest.mark.parametrize( - "temp,unit,translation", - ( + ("temp", "unit", "translation"), + [ ("20", "F", "20°F (-7°C)"), ("M20", "F", "-20°F (-29°C)"), ("20", "C", "20°C (68°F)"), ("M20", "C", "-20°C (-4°F)"), ("", "F", ""), - ), + ], ) -def test_temperature(temp: str, unit: str, translation: str): - """Tests temperature translation and conversion""" +def test_temperature(temp: str, unit: str, translation: str) -> None: + """Test temperature translation and conversion.""" assert translate.base.temperature(core.make_number(temp), unit) == translation -def test_metar(): # sourcery skip: dict-assign-update-to-union - """Tests end-to-end METAR translation""" +def test_metar() -> None: + """Test end-to-end METAR translation.""" units = structs.Units.north_american() empty_fields = ( "raw", @@ -200,9 +186,8 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union core.make_number("020"), ], "wx_codes": get_wx_codes(["+RA"])[1], - } - data.update({k: "" for k in empty_fields}) - data = structs.MetarData(**data) + } | {k: "" for k in empty_fields} + metar_data = structs.MetarData(**data) # type: ignore trans = structs.MetarTrans( altimeter="29.92 inHg (1013 hPa)", clouds="Broken layer at 1500ft (Cumulonimbus) - Reported AGL", @@ -213,7 +198,7 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union wind="N-360 (variable 340 to 020) at 12kt gusting to 20kt", wx_codes="Heavy Rain", ) - translated = translate.metar.translate_metar(data, units) + translated = translate.metar.translate_metar(metar_data, units) assert isinstance(translated, structs.MetarTrans) assert translated == trans @@ -222,21 +207,21 @@ def test_metar(): # sourcery skip: dict-assign-update-to-union @pytest.mark.parametrize( - "shear,translation", - ( + ("shear", "translation"), + [ ("", ""), ("WS020/07040KT", "Wind shear 2000ft from 070 at 40kt"), ("WS100/20020KT", "Wind shear 10000ft from 200 at 20kt"), - ), + ], ) -def test_wind_shear(shear: str, translation: str): - """Tests wind shear unpacking and translation""" +def test_wind_shear(shear: str, translation: str) -> None: + """Test wind shear unpacking and translation.""" assert translate.taf.wind_shear(shear) == translation @pytest.mark.parametrize( - "turb_ice,translation", - ( + ("turb_ice", "translation"), + [ ([], ""), ( ["540553"], @@ -247,29 +232,29 @@ def test_wind_shear(shear: str, translation: str): ["610023", "610062"], "Light icing from 200ft to 3200ft, Light icing from 600ft to 2600ft", ), - ), + ], ) -def test_turb_ice(turb_ice: List[str], translation: str): - """Tests turbulence and icing translations""" +def test_turb_ice(turb_ice: list[str], translation: str) -> None: + """Test turbulence and icing translations.""" assert translate.taf.turb_ice(turb_ice) == translation @pytest.mark.parametrize( - "temp,translation", - ( + ("temp", "translation"), + [ ("", ""), ("TX20/1518Z", "Maximum temperature of 20°C (68°F) at 15-18:00Z"), ("TXM02/04", "Maximum temperature of -2°C (28°F) at 04:00Z"), ("TN00/00", "Minimum temperature of 0°C (32°F) at 00:00Z"), - ), + ], ) -def test_min_max_temp(temp: str, translation: str): - """Tests temperature time translation and conversion""" +def test_min_max_temp(temp: str, translation: str) -> None: + """Test temperature time translation and conversion.""" assert translate.taf.min_max_temp(temp) == translation -def test_taf(): - """Tests end-to-end TAF translation""" +def test_taf() -> None: + """Test end-to-end TAF translation.""" units = structs.Units.north_american() empty_line_fields = ( "raw", @@ -309,7 +294,7 @@ def test_taf(): line_data[key] = "" for key in empty_fields: data[key] = "" - data = structs.TafData(forecast=[structs.TafLineData(**line_data)], **data) + taf_data = structs.TafData(forecast=[structs.TafLineData(**line_data)], **data) # type: ignore line_trans = structs.TafLineTrans( altimeter="29.92 inHg (1013 hPa)", clouds="Broken layer at 1500ft (Cumulonimbus) - Reported AGL", @@ -326,7 +311,7 @@ def test_taf(): min_temp="Minimum temperature of 0°C (32°F) at 00:00Z", remarks={}, ) - translated = translate.taf.translate_taf(data, units) + translated = translate.taf.translate_taf(taf_data, units) assert isinstance(translated, structs.TafTrans) for line in translated.forecast: assert isinstance(line, structs.TafLineTrans) @@ -337,8 +322,8 @@ def test_taf(): @pytest.mark.parametrize( - "rmk,out", - ( + ("rmk", "out"), + [ ( "RMK AO1 ACFT MSHP SLP137 T02720183 BINOVC", { @@ -369,9 +354,9 @@ def test_taf(): "TSB20": "Thunderstorm began at :20", }, ), - ), + ], ) -def test_translate(rmk: str, out: Dict[str, str]): +def test_translate(rmk: str, out: dict[str, str]) -> None: """Tests extracting translations from the remarks string""" data = remarks.parse(rmk) assert translate.remarks.translate(rmk, data) == out diff --git a/tests/service/test_base.py b/tests/service/test_base.py index 9836758..11cae9a 100644 --- a/tests/service/test_base.py +++ b/tests/service/test_base.py @@ -1,11 +1,8 @@ -""" -Service API Tests -""" - -# pylint: disable=missing-class-docstring +"""Service API Tests.""" # stdlib -from typing import Any, Tuple + +from typing import Any # library import pytest @@ -19,7 +16,7 @@ class BaseServiceTest: service_class = service.Service report_type: str = "metar" - required_attrs: Tuple[str] = tuple() + required_attrs: tuple[str, ...] = () @pytest.fixture def serv(self) -> service.Service: @@ -27,8 +24,8 @@ def serv(self) -> service.Service: class ServiceClassTest(BaseServiceTest): - def test_init(self, serv: service.Service): - """Tests that the Service class is initialized properly""" + def test_init(self, serv: service.Service) -> None: + """Test that the Service class is initialized properly.""" for attr in BASE_ATTRS + self.required_attrs: assert hasattr(serv, attr) is True assert serv.report_type == self.report_type @@ -39,13 +36,13 @@ def validate_report(self, station: str, report: Any) -> None: assert isinstance(report, str) assert station in report - def test_fetch(self, station: str, serv: service.Service): - """Tests that reports are fetched from service""" - report = serv.fetch(station) + def test_fetch(self, station: str, serv: service.Service) -> None: + """Test that reports are fetched from service.""" + report = serv.fetch(station) # type: ignore self.validate_report(station, report) @pytest.mark.asyncio - async def test_async_fetch(self, station: str, serv: service.Service): - """Tests that reports are fetched from async service""" - report = await serv.async_fetch(station) + async def test_async_fetch(self, station: str, serv: service.Service) -> None: + """Test that reports are fetched from async service.""" + report = await serv.async_fetch(station) # type: ignore self.validate_report(station, report) diff --git a/tests/service/test_bulk.py b/tests/service/test_bulk.py index 86ed825..54c8ec4 100644 --- a/tests/service/test_bulk.py +++ b/tests/service/test_bulk.py @@ -1,9 +1,4 @@ -""" -Bulk Service Tests -""" - -# stdlib -from typing import Tuple +"""Bulk Service Tests.""" # library import pytest @@ -16,35 +11,34 @@ class BulkServiceTest(ServiceClassTest): - """Tests bulk downloads from NOAA file server""" + """Test bulk downloads from NOAA file server.""" - service_class: bulk.Service - report_types: Tuple[str, ...] + report_types: tuple[str, ...] @property - def report_type(self) -> str: + def report_type(self) -> str: # type: ignore return self.report_types[0] - def test_fetch(self): - """Tests that reports are fetched from service""" - reports = self.service_class(self.report_type).fetch() + def test_fetch(self) -> None: + """Test that reports are fetched from service.""" + reports = self.service_class(self.report_type).fetch() # type: ignore assert isinstance(reports, list) assert bool(reports) @pytest.mark.asyncio - async def test_async_fetch(self): - """Tests that reports are fetched from async service""" + async def test_async_fetch(self) -> None: + """Test that reports are fetched from async service.""" for report_type in self.report_types: service = self.service_class(report_type) - reports = await service.async_fetch() + reports = await service.async_fetch() # type: ignore assert isinstance(reports, list) assert bool(reports) class TestNOAABulk(BulkServiceTest): - """Tests bulk downloads from NOAA file server""" + """Test bulk downloads from NOAA file server.""" - service_class = bulk.NOAA_Bulk + service_class = bulk.NoaaBulk report_types = ( "metar", "taf", @@ -54,7 +48,7 @@ class TestNOAABulk(BulkServiceTest): class TestIntlBulk(BulkServiceTest): - """Tests bulk downloads from NOAA file server""" + """Test bulk downloads from NOAA file server.""" - service_class = bulk.NOAA_Intl + service_class = bulk.NoaaIntl report_types = ("airsigmet",) diff --git a/tests/service/test_files.py b/tests/service/test_files.py index 8d62815..14ae308 100644 --- a/tests/service/test_files.py +++ b/tests/service/test_files.py @@ -1,8 +1,6 @@ -""" -FileService API Tests -""" +"""FileService API Tests.""" -# pylint: disable=protected-access,missing-class-docstring,unidiomatic-typecheck +# ruff: noqa: SLF001 # library import pytest @@ -24,40 +22,39 @@ class TestScrapeService(ServiceClassTest): "update", ) - def test_file_service_not_implemented(self, serv: service.Service): - """Tests that the base FileService class throws NotImplemented errors""" + def test_file_service_not_implemented(self, serv: service.Service) -> None: + """Test that the base FileService class throws NotImplemented errors.""" if type(serv) != service.files.FileService: return - # pylint: disable=no-member.pointless-statement with pytest.raises(NotImplementedError): - serv._extract(None, None) + serv._extract(None, None) # type: ignore with pytest.raises(NotImplementedError): - serv._urls + assert serv._urls @pytest.mark.asyncio - async def test_fetch_bad_station(self, serv: service.Service): - """Tests fetch exception handling""" + async def test_fetch_bad_station(self, serv: service.Service) -> None: + """Test fetch exception handling.""" for station in ("12K", "MAYT"): with pytest.raises(exceptions.BadStation): - await serv.async_fetch(station) # pylint: disable=no-member + await serv.async_fetch(station) # type: ignore @pytest.mark.asyncio - async def test_scrape_service_not_implemented(self, serv: service.Service): - """Should raise exception due to empty url""" + async def test_scrape_service_not_implemented(self, serv: service.Service) -> None: + """Should raise exception due to empty url.""" if type(serv) == service.scrape.ScrapeService: with pytest.raises(NotImplementedError): - await serv.async_fetch("KJFK") # pylint: disable=no-member + await serv.async_fetch("KJFK") # type: ignore @pytest.mark.parametrize("station", ("KJFK", "KMCO", "PHNL")) class TestNBM(ServiceFetchTest): - service_class = service.NOAA_NBM + service_class = service.NoaaNbm report_type = "nbs" -def test_nbm_all(): - """Tests extracting all reports from the requested file""" - reports = service.NOAA_NBM("nbs").all +def test_nbm_all() -> None: + """Test extracting all reports from the requested file.""" + reports = service.NoaaNbm("nbs").all assert isinstance(reports, list) assert len(reports) > 0 diff --git a/tests/service/test_scrape.py b/tests/service/test_scrape.py index 72d6a0a..b8a68b0 100644 --- a/tests/service/test_scrape.py +++ b/tests/service/test_scrape.py @@ -1,11 +1,9 @@ -""" -ScrapeService API Tests -""" +"""ScrapeService API Tests.""" -# pylint: disable=protected-access,missing-class-docstring,unidiomatic-typecheck +# ruff: noqa: SLF001 # stdlib -from typing import Any, Tuple +from typing import Any # library import pytest @@ -21,16 +19,14 @@ class TestStationScrape(ServiceClassTest): service_class = service.scrape.StationScrape required_attrs = ("method", "_strip_whitespace", "_extract") - def test_service(self, serv: service.scrape.ScrapeService): - """Tests for expected values and method implementation""" - # pylint: disable=no-member + def test_service(self, serv: service.scrape.ScrapeService) -> None: + """Test for expected values and method implementation.""" assert isinstance(serv._url, str) assert isinstance(serv.method, str) assert serv.method in {"GET", "POST"} - def test_make_err(self, serv: service.scrape.ScrapeService): - """Tests that InvalidRequest exceptions are generated with the right message""" - # pylint: disable=no-member + def test_make_err(self, serv: service.scrape.ScrapeService) -> None: + """Test that InvalidRequest exceptions are generated with the right message.""" key, msg = "test_key", "testing" name = serv.__class__.__name__ err = serv._make_err(msg, key) @@ -39,140 +35,140 @@ def test_make_err(self, serv: service.scrape.ScrapeService): assert err.args == (err_str,) assert str(err) == err_str - def test_fetch_bad_station(self, serv: service.scrape.ScrapeService): - """Tests fetch exception handling""" + def test_fetch_bad_station(self, serv: service.scrape.ScrapeService) -> None: + """Test fetch exception handling.""" for station in ("12K", "MAYT"): with pytest.raises(exceptions.BadStation): - serv.fetch(station) # pylint: disable=no-member + serv.fetch(station) # type: ignore - def test_not_implemented(self, serv: service.scrape.ScrapeService): - """Should raise exception due to empty url""" + def test_not_implemented(self, serv: service.scrape.ScrapeService) -> None: + """Should raise exception due to empty url.""" if type(serv) == service.scrape.ScrapeService: with pytest.raises(NotImplementedError): - serv.fetch("KJFK") # pylint: disable=no-member + serv.fetch("KJFK") # type: ignore @pytest.mark.asyncio - async def test_async_fetch_bad_station(self, serv: service.scrape.ScrapeService): - """Tests fetch exception handling""" + async def test_async_fetch_bad_station(self, serv: service.scrape.ScrapeService) -> None: + """Test fetch exception handling.""" for station in ("12K", "MAYT"): with pytest.raises(exceptions.BadStation): - await serv.async_fetch(station) # pylint: disable=no-member + await serv.async_fetch(station) # type: ignore @pytest.mark.asyncio - async def test_async_not_implemented(self, serv: service.scrape.ScrapeService): - """Should raise exception due to empty url""" + async def test_async_not_implemented(self, serv: service.scrape.ScrapeService) -> None: + """Should raise exception due to empty url.""" if type(serv) == service.scrape.ScrapeService: with pytest.raises(NotImplementedError): - await serv.async_fetch("KJFK") # pylint: disable=no-member + await serv.async_fetch("KJFK") # type: ignore -NOAA_PARAMS = ("station", ("KJFK", "EGLL", "PHNL")) +NOAA_PARAMS = ("station", ["KJFK", "EGLL", "PHNL"]) @pytest.mark.parametrize(*NOAA_PARAMS) -class TestNOAA(ServiceFetchTest): - service_class = service.NOAA +class TestNoaa(ServiceFetchTest): + service_class = service.Noaa -class TestNOAAClass(TestStationScrape): - service_class = service.NOAA +class TestNoaaClass(TestStationScrape): + service_class = service.Noaa @pytest.mark.parametrize(*NOAA_PARAMS) -class TestNOAATaf(ServiceFetchTest): - service_class = service.NOAA +class TestNoaaTaf(ServiceFetchTest): + service_class = service.Noaa report_type = "taf" @pytest.mark.parametrize(*NOAA_PARAMS) -class TestNOAA_FTP(ServiceFetchTest): - service_class = service.scrape.NOAA_FTP +class TestNoaaFtp(ServiceFetchTest): + service_class = service.scrape.NoaaFtp -class TestNOAA_FTPClass(TestStationScrape): - service_class = service.scrape.NOAA_FTP +class TestNoaaFtpClass(TestStationScrape): + service_class = service.scrape.NoaaFtp @pytest.mark.parametrize(*NOAA_PARAMS) -class TestNOAA_Scrape(ServiceFetchTest): - service_class = service.scrape.NOAA_Scrape +class TestNoaaScrape(ServiceFetchTest): + service_class = service.scrape.NoaaScrape -class TestNOAA_ScrapeClass(TestStationScrape): - service_class = service.scrape.NOAA_Scrape +class TestNoaaScrapeClass(TestStationScrape): + service_class = service.scrape.NoaaScrape @pytest.mark.parametrize(*NOAA_PARAMS) -class TestNOAA_ScrapeList(ServiceFetchTest): - service_class = service.scrape.NOAA_ScrapeList +class TestNoaaScrapeList(ServiceFetchTest): + service_class = service.scrape.NoaaScrapeList report_type = "pirep" - def validate_report(self, station: str, report: Any) -> None: + def validate_report(self, station: str, report: Any) -> None: # noqa: ARG002 assert isinstance(report, list) assert isinstance(report[0], str) -class TestNOAA_ScrapeListClass(TestStationScrape): - service_class = service.scrape.NOAA_ScrapeList +class TestNoaaScrapeListClass(TestStationScrape): + service_class = service.scrape.NoaaScrapeList # type: ignore report_type = "pirep" -# @pytest.mark.parametrize("station", ("RKSI", "RKSS", "RKNY")) -# class TestAMO(TestStationScrape): -# service_class = service.AMO +# @pytest.mark.parametrize("station", ["RKSI", "RKSS", "RKNY"]) +# class TestAmo(TestStationScrape): +# service_class = service.Amo # report_type = "metar" -# class TestAMOClass(TestStationScrape): -# service_class = service.AMO +# class TestAmoClass(TestStationScrape): +# service_class = service.Amo -@pytest.mark.parametrize("station", ("SKBO",)) -class TestMAC(ServiceFetchTest): - service_class = service.MAC +@pytest.mark.parametrize("station", ["SKBO"]) +class TestMac(ServiceFetchTest): + service_class = service.Mac -class TestMACClass(TestStationScrape): - service_class = service.MAC +class TestMacClass(TestStationScrape): + service_class = service.Mac -@pytest.mark.parametrize("station", ("YBBN", "YSSY", "YCNK")) -class TestAUBOM(ServiceFetchTest): - service_class = service.AUBOM +@pytest.mark.parametrize("station", ["YBBN", "YSSY", "YCNK"]) +class TestAubom(ServiceFetchTest): + service_class = service.Aubom -class TestAUBOMClass(TestStationScrape): - service_class = service.AUBOM +class TestAubomClass(TestStationScrape): + service_class = service.Aubom -@pytest.mark.parametrize("station", ("VAPO", "VEGT")) -class TestOLBS(ServiceFetchTest): - service_class = service.OLBS +@pytest.mark.parametrize("station", ["VAPO", "VEGT"]) +class TestOlbs(ServiceFetchTest): + service_class = service.Olbs -class TestOLBSClass(TestStationScrape): - service_class = service.OLBS +class TestOlbsClass(TestStationScrape): + service_class = service.Olbs -@pytest.mark.parametrize("station", ("EHAM", "ENGM", "BIRK")) -class TestNAM(ServiceFetchTest): - service_class = service.NAM +@pytest.mark.parametrize("station", ["EHAM", "ENGM", "BIRK"]) +class TestNam(ServiceFetchTest): + service_class = service.Nam -class TestNAMClass(TestStationScrape): - service_class = service.NAM +class TestNamClass(TestStationScrape): + service_class = service.Nam -# @pytest.mark.parametrize("station", ("ZJQH", "ZYCC", "ZSWZ")) -# class TestAVT(ServiceFetchTest): -# service_class = service.AVT +# @pytest.mark.parametrize("station", ["ZJQH", "ZYCC", "ZSWZ"]) +# class TestAvt(ServiceFetchTest): +# service_class = service.Avt -# class TestAVTClass(TestStationScrape): -# service_class = service.AVT +# class TestAvtClass(TestStationScrape): +# service_class = service.Avt @pytest.mark.parametrize(*NOAA_PARAMS) class TestNotam(ServiceFetchTest): - service_class = service.FAA_NOTAM + service_class = service.FaaNotam report_type = "notam" def validate_report(self, station: str, report: Any) -> None: @@ -182,19 +178,19 @@ def validate_report(self, station: str, report: Any) -> None: @pytest.mark.parametrize( - "stations,country,serv", - ( - (("KJFK", "PHNL"), "US", service.NOAA), - (("EGLL",), "GB", service.NOAA), - (("RKSI",), "KR", service.AMO), - (("SKBO", "SKPP"), "CO", service.MAC), - (("YWOL", "YSSY"), "AU", service.AUBOM), - (("VAPO", "VEGT"), "IN", service.OLBS), - # (("ZJQH", "ZYCC", "ZSWZ"), "CN", service.AVT), - ), + ("stations", "country", "serv"), + [ + (("KJFK", "PHNL"), "US", service.Noaa), + (("EGLL",), "GB", service.Noaa), + (("RKSI",), "KR", service.Amo), + (("SKBO", "SKPP"), "CO", service.Mac), + (("YWOL", "YSSY"), "AU", service.Aubom), + (("VAPO", "VEGT"), "IN", service.Olbs), + # (("ZJQH", "ZYCC", "ZSWZ"), "CN", service.Avt), + ], ) -def test_get_service(stations: Tuple[str], country: str, serv: service.Service): - """Tests that the correct service class is returned""" +def test_get_service(stations: tuple[str], country: str, serv: service.Service) -> None: + """Test that the correct service class is returned.""" for station in stations: - fetched = service.get_service(station, country)("metar") - assert isinstance(fetched, serv) + fetched = service.get_service(station, country)("metar") # type: ignore + assert isinstance(fetched, serv) # type: ignore diff --git a/tests/test_base.py b/tests/test_base.py index 6f5edeb..a1b5e39 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,20 +1,18 @@ -""" -AVWX Base class tests -""" +"""AVWX Base class tests.""" # library import pytest # module -from avwx import base, Station +from avwx import Station, base -@pytest.mark.parametrize("code", ("KMCO", "MCO")) -def test_find_station(code: str): +@pytest.mark.parametrize("code", ["KMCO", "MCO"]) +def test_find_station(code: str) -> None: station = base.find_station(f"1 2 {code} 4") assert isinstance(station, Station) assert station.icao == "KMCO" -def test_no_station(): +def test_no_station() -> None: assert base.find_station("1 2 3 4") is None diff --git a/tests/test_flight_path.py b/tests/test_flight_path.py index 445795c..2aa01b5 100644 --- a/tests/test_flight_path.py +++ b/tests/test_flight_path.py @@ -1,9 +1,6 @@ -""" -Flight path tests -""" +"""Flight path tests.""" -# stdlib -from typing import List, Tuple, Union +from __future__ import annotations # library import pytest @@ -12,7 +9,12 @@ from avwx import flight_path from avwx.structs import Coord -FLIGHT_PATHS = ( +FloatCoordT = tuple[float, float, str] +FloatPathT = FloatCoordT | str +CoordPathT = Coord | str +TestCaseT = tuple[list[FloatPathT], list[FloatCoordT]] + +FLIGHT_PATHS: list[TestCaseT] = [ ( [(12.34, -12.34, "12.34,-12.34"), (-43.21, 43.21, "-43.21,43.21")], [(12.34, -12.34, "12.34,-12.34"), (-43.21, 43.21, "-43.21,43.21")], @@ -59,25 +61,24 @@ (28.43, -81.31, "KMCO"), ], ), -) - - -COORD = Tuple[float, float, str] -COORD_SRC = List[Union[COORD, str]] +] -def _to_coord(coords: COORD_SRC) -> List[Union[Coord, str]]: - for i, item in enumerate(coords): +def _to_coord(paths: list[FloatPathT]) -> list[CoordPathT]: + coords: list[CoordPathT] = [] + for item in paths: if isinstance(item, tuple): - coords[i] = Coord(lat=item[0], lon=item[1], repr=item[2]) + coords.append(Coord(lat=item[0], lon=item[1], repr=item[2])) + else: + coords.append(item) return coords -@pytest.mark.parametrize("source,target", FLIGHT_PATHS) -def test_to_coordinates(source: List[str], target: COORD_SRC): - """Test coord routing from coords, stations, and navaids""" - source = _to_coord(source) - coords = flight_path.to_coordinates(source) +@pytest.mark.parametrize(("source", "target"), FLIGHT_PATHS) +def test_to_coordinates(source: list[FloatPathT], target: list[FloatPathT]) -> None: + """Test coord routing from coords, stations, and navaids.""" + coord_path = _to_coord(source) + coords = flight_path.to_coordinates(coord_path) # Round to prevent minor coord changes from breaking tests - coords = [(round(c.lat, 2), round(c.lon, 2), c.repr) for c in coords] - assert coords == target + float_path: list[FloatPathT] = [(round(c.lat, 2), round(c.lon, 2), c.repr or "") for c in coords] + assert float_path == target diff --git a/tests/test_station.py b/tests/test_station.py index 83295e2..458dd3a 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,9 +1,11 @@ -""" -Station Data Tests -""" +"""Station Data Tests.""" + +# ruff: noqa: FBT001 # stdlib -from typing import Any, Optional +from __future__ import annotations + +from typing import Any # library import pytest @@ -11,16 +13,15 @@ # module from avwx import exceptions, station - NA_CODES = {"KJFK", "PHNL", "TNCM", "MYNN"} IN_CODES = {"EGLL", "MNAH", "MUHA"} BAD_CODES = {"12K", "MAYT"} -def test_station_list(): - """Test reporting filter for full station list""" +def test_station_list() -> None: + """Test reporting filter for full station list.""" reporting = station.station_list() - full = station.station_list(False) + full = station.station_list(reporting=False) assert len(full) > len(reporting) @@ -29,21 +30,21 @@ def test_station_list(): @pytest.mark.parametrize("code", NA_CODES) -def test_is_na_format(code: str): +def test_is_na_format(code: str) -> None: assert station.uses_na_format(code) is True -def test_na_format_default(): +def test_na_format_default() -> None: assert station.uses_na_format("XXXX", True) is True @pytest.mark.parametrize("code", IN_CODES) -def test_is_in_format(code: str): +def test_is_in_format(code: str) -> None: assert station.uses_na_format(code) is False @pytest.mark.parametrize("code", BAD_CODES) -def test_is_bad_format(code: str): +def test_is_bad_format(code: str) -> None: with pytest.raises(exceptions.BadStation): station.uses_na_format(code) @@ -53,19 +54,20 @@ def test_is_bad_format(code: str): @pytest.mark.parametrize("code", NA_CODES | IN_CODES) -def test_is_valid_station(code: str): - assert station.valid_station(code) is None +def test_is_valid_station(code: str) -> None: + station.valid_station(code) @pytest.mark.parametrize("code", BAD_CODES) -def test_is_invalid_station(code: str): +def test_is_invalid_station(code: str) -> None: with pytest.raises(exceptions.BadStation): station.valid_station(code) -def test_nearest(): - """Tests returning nearest Stations to lat, lon""" +def test_nearest() -> None: + """Test returning nearest Stations to lat, lon.""" dist = station.nearest(28.43, -81.31) + assert isinstance(dist, dict) stn = dist.pop("station") assert isinstance(stn, station.Station) assert stn.icao == "KMCO" @@ -74,36 +76,36 @@ def test_nearest(): # These airport counts may change during data updates but should be valid otherwise @pytest.mark.parametrize( - "params,count", - ( - ((30, -82, 10, True, True, 0.3), 1), - ((30, -82, 10, True, False, 0.3), 5), - ((30, -82, 10, False, False, 0.3), 7), - ((30, -82, 1000, True, True, 0.5), 5), - ((30, -82, 1000, False, False, 0.5), 37), - ), + ("lat", "lon", "n", "airport", "reporting", "dist", "count"), + [ + (30, -82, 10, True, True, 0.3, 1), + (30, -82, 10, True, False, 0.3, 5), + (30, -82, 10, False, False, 0.3, 7), + (30, -82, 1000, True, True, 0.5, 5), + (30, -82, 1000, False, False, 0.5, 37), + ], ) -def test_nearest_params(params: tuple, count: int): - stations = station.nearest(*params) +def test_nearest_params(lat: int, lon: int, n: int, airport: bool, reporting: bool, dist: float, count: int) -> None: + stations = station.nearest(lat, lon, n, is_airport=airport, sends_reports=reporting, max_coord_distance=dist) assert len(stations) >= count - for dist in stations: - stn = dist.pop("station") + for distance in stations: + stn = distance.pop("station") assert isinstance(stn, station.Station) - assert all(isinstance(val, float) for val in dist.values()) + assert all(isinstance(val, float) for val in distance.values()) @pytest.mark.parametrize( - "airport,reports,count", - ( + ("airport", "reports", "count"), + [ (True, True, 6), (True, False, 16), (False, True, 6), (False, False, 30), - ), + ], ) -def test_nearest_filter(airport: bool, reports: bool, count: int): - """Tests filtering nearest stations""" - stations = station.nearest(30, -80, 30, airport, reports, 1.5) +def test_nearest_filter(airport: bool, reports: bool, count: int) -> None: + """Test filtering nearest stations.""" + stations = station.nearest(30, -80, 30, is_airport=airport, sends_reports=reports, max_coord_distance=1.5) assert len(stations) == count @@ -112,8 +114,8 @@ def test_nearest_filter(airport: bool, reports: bool, count: int): BAD_STATION_CODES = {"1234", 1234, None, True, ""} -def test_storage_code(): - """Tests ID code selection""" +def test_storage_code() -> None: + """Test ID code selection.""" stn = station.Station.from_icao("KJFK") stn.icao = "ICAO" stn.iata = "IATA" @@ -128,21 +130,21 @@ def test_storage_code(): assert stn.storage_code == "LOCAL" stn.local = None with pytest.raises(exceptions.BadStation): - stn.storage_code + assert stn.storage_code @pytest.mark.parametrize( - "icao,name,city", - ( + ("icao", "name", "city"), + [ ("KJFK", "John F Kennedy International Airport", "New York"), ("kjfk", "John F Kennedy International Airport", "New York"), ("KLAX", "Los Angeles International Airport", "Los Angeles"), ("PHNL", "Daniel K Inouye International Airport", "Honolulu"), ("EGLL", "London Heathrow Airport", "London"), - ), + ], ) -def test_from_icao(icao: str, name: str, city: str): - """Tests loading a Station by ICAO ident""" +def test_from_icao(icao: str, name: str, city: str) -> None: + """Test loading a Station by ICAO ident.""" stn = station.Station.from_icao(icao) assert isinstance(stn, station.Station) assert icao.upper() == stn.icao @@ -151,23 +153,23 @@ def test_from_icao(icao: str, name: str, city: str): @pytest.mark.parametrize("code", BAD_STATION_CODES | {"KX07", "TX35"}) -def test_from_bad_icao(code: Any): +def test_from_bad_icao(code: Any) -> None: with pytest.raises(exceptions.BadStation): station.Station.from_icao(code) @pytest.mark.parametrize( - "iata,icao", - ( + ("iata", "icao"), + [ ("JFK", "KJFK"), ("jfk", "KJFK"), ("LAX", "KLAX"), ("HNL", "PHNL"), ("LHR", "EGLL"), - ), + ], ) -def test_from_iata(iata: str, icao: str): - """Tests loading a Station by IATA code""" +def test_from_iata(iata: str, icao: str) -> None: + """Test loading a Station by IATA code.""" stn = station.Station.from_iata(iata) assert isinstance(stn, station.Station) assert iata.upper() == stn.iata @@ -175,22 +177,22 @@ def test_from_iata(iata: str, icao: str): @pytest.mark.parametrize("code", BAD_STATION_CODES | {"KMCO", "X35"}) -def test_from_bad_iata(code: Any): +def test_from_bad_iata(code: Any) -> None: with pytest.raises(exceptions.BadStation): station.Station.from_iata(code) @pytest.mark.parametrize( - "gps,icao,name", - ( + ("gps", "icao", "name"), + [ ("KJFK", "KJFK", "John F Kennedy International Airport"), ("kjfk", "KJFK", "John F Kennedy International Airport"), ("EGLL", "EGLL", "London Heathrow Airport"), ("KX07", None, "Lake Wales Municipal Airport"), - ), + ], ) -def test_from_gps(gps: str, icao: Optional[str], name: str): - """Tests loading a Station by GPS code""" +def test_from_gps(gps: str, icao: str | None, name: str) -> None: + """Test loading a Station by GPS code.""" stn = station.Station.from_gps(gps) assert isinstance(stn, station.Station) assert gps.upper() == stn.gps @@ -199,14 +201,14 @@ def test_from_gps(gps: str, icao: Optional[str], name: str): @pytest.mark.parametrize("code", BAD_STATION_CODES | {"KX35"}) -def test_from_bad_gps(code: Any): +def test_from_bad_gps(code: Any) -> None: with pytest.raises(exceptions.BadStation): station.Station.from_gps(code) @pytest.mark.parametrize( - "code,icao,name", - ( + ("code", "icao", "name"), + [ ("KJFK", "KJFK", "John F Kennedy International Airport"), ("kjfk", "KJFK", "John F Kennedy International Airport"), ("EGLL", "EGLL", "London Heathrow Airport"), @@ -214,10 +216,10 @@ def test_from_bad_gps(code: Any): ("LAX", "KLAX", "Los Angeles International Airport"), ("HNL", "PHNL", "Daniel K Inouye International Airport"), ("KX07", None, "Lake Wales Municipal Airport"), - ), + ], ) -def test_from_code(code: str, icao: Optional[str], name: str): - """Tests loading a Station by any code""" +def test_from_code(code: str, icao: str | None, name: str) -> None: + """Test loading a Station by any code.""" stn = station.Station.from_code(code) assert isinstance(stn, station.Station) assert icao == stn.icao @@ -225,47 +227,51 @@ def test_from_code(code: str, icao: Optional[str], name: str): @pytest.mark.parametrize("code", BAD_STATION_CODES) -def test_from_bad_code(code: Any): +def test_from_bad_code(code: Any) -> None: with pytest.raises(exceptions.BadStation): station.Station.from_code(code) -@pytest.mark.parametrize( - "lat,lon,icao", ((28.43, -81.31, "KMCO"), (28.43, -81, "KTIX")) -) -def test_station_nearest(lat: float, lon: float, icao: str): - """Tests loading a Station nearest to a lat,lon coordinate pair""" - stn, dist = station.Station.nearest(lat, lon, is_airport=True) +@pytest.mark.parametrize(("lat", "lon", "icao"), [(28.43, -81.31, "KMCO"), (28.43, -81, "KTIX")]) +def test_station_nearest(lat: float, lon: float, icao: str) -> None: + """Test loading a Station nearest to a lat,lon coordinate pair.""" + resp = station.Station.nearest(lat, lon, is_airport=True) + assert resp is not None + stn, dist = resp assert isinstance(stn, station.Station) assert stn.icao == icao assert all(isinstance(val, float) for val in dist.values()) -def test_station_nearest_ip(): - """Tests loading a Station nearest to IP location""" - stn, dist = station.Station.nearest() +def test_station_nearest_ip() -> None: + """Test loading a Station nearest to IP location.""" + resp = station.Station.nearest() + assert resp is not None + stn, dist = resp assert isinstance(stn, station.Station) assert all(isinstance(val, float) for val in dist.values()) -def test_station_nearest_without_iata(): - """Test nearest station with IATA req disabled""" - stn, dist = station.Station.nearest(28.43, -81, False, False) +def test_station_nearest_without_iata() -> None: + """Test nearest station with IATA req disabled.""" + resp = station.Station.nearest(28.43, -81, is_airport=False, sends_reports=False) + assert resp is not None + stn, dist = resp assert isinstance(stn, station.Station) assert stn.lookup_code == "FA18" assert all(isinstance(val, float) for val in dist.values()) @pytest.mark.parametrize( - "code,near", - ( + ("code", "near"), + [ ("KMCO", "KORL"), ("KJFK", "KLGA"), ("PHKO", "PHSF"), - ), + ], ) -def test_nearby(code: str, near: str): - """Tests finding nearby airports to the current one""" +def test_nearby(code: str, near: str) -> None: + """Tests finding nearby airports to the current one.""" target = station.Station.from_code(code) nearby = target.nearby() assert isinstance(nearby, list) @@ -276,16 +282,16 @@ def test_nearby(code: str, near: str): assert nearest[0].lookup_code == near -@pytest.mark.parametrize("code", ("KJFK", "EGLL")) -def test_sends_reports(code: str): - """Tests bool indicating likely reporting station""" +@pytest.mark.parametrize("code", ["KJFK", "EGLL"]) +def test_sends_reports(code: str) -> None: + """Test bool indicating likely reporting station.""" stn = station.Station.from_code(code) assert stn.sends_reports is True assert stn.iata is not None -@pytest.mark.parametrize("code", ("FA18",)) -def test_not_sends_reports(code: str): +@pytest.mark.parametrize("code", ["FA18"]) +def test_not_sends_reports(code: str) -> None: stn = station.Station.from_code(code) assert stn.sends_reports is False assert stn.iata is None @@ -294,47 +300,47 @@ def test_not_sends_reports(code: str): # Test station search -@pytest.mark.parametrize("icao", ("KMCO", "KJFK", "PHNL", "EGLC")) -def test_exact_icao(icao: str): - """Tests searching for a Station by exact ICAO ident""" +@pytest.mark.parametrize("icao", ["KMCO", "KJFK", "PHNL", "EGLC"]) +def test_exact_icao(icao: str) -> None: + """Test searching for a Station by exact ICAO ident.""" results = station.search(icao) assert len(results) == 10 assert results[0].icao == icao @pytest.mark.parametrize( - "code,gps", - ( + ("code", "gps"), + [ ("MCO", "KMCO"), ("1A5", "K1A5"), - ), + ], ) -def test_secondary_code(code: str, gps: str): - """Tests searching for a Station by non-ICAO codes""" +def test_secondary_code(code: str, gps: str) -> None: + """Test searching for a Station by non-ICAO codes.""" results = station.search(code) assert len(results) == 10 assert results[0].gps == gps @pytest.mark.parametrize( - "text,icao", - ( + ("text", "icao"), + [ ("kona hi", "PHKO"), ("danville powell field", "KDVK"), ("lexington ky", "KLEX"), ("orlando", "KMCO"), ("london city", "EGLC"), - ), + ], ) -def test_combined_terms(text: str, icao: str): - """Tests search using multiple search terms""" +def test_combined_terms(text: str, icao: str) -> None: + """Test search using multiple search terms.""" results = station.search(text) assert len(results) == 10 assert results[0].lookup_code == icao -def test_search_filter(): - """Tests search result filtering""" +def test_search_filter() -> None: + """Test search result filtering.""" for airport in station.search("orlando", is_airport=True): assert "airport" in airport.type for airport in station.search("orlando", sends_reports=True): diff --git a/tests/util.py b/tests/util.py index 1a0bc81..c4de337 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,24 +1,28 @@ -""" -Testing utilities -""" - -# pylint: disable=redefined-builtin,invalid-name +"""Testing utilities.""" # stdlib +from __future__ import annotations + import json from contextlib import suppress -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path -from typing import Any, Iterator, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any # module from avwx import structs +if TYPE_CHECKING: + from collections.abc import Iterator + def assert_number( - num: structs.Number, repr: str, value: object = None, spoken: str = None -): - """Tests string conversion into a Number dataclass""" + num: structs.Number | None, + repr: str, # noqa: A002 + value: Any | None = None, + spoken: str | None = None, +) -> None: + """Test string conversion into a Number dataclass.""" if not repr: assert num is None else: @@ -29,8 +33,8 @@ def assert_number( assert num.spoken == spoken -def assert_timestamp(ts: structs.Timestamp, repr: str, value: datetime): - """Tests string conversion into a Timestamp dataclass""" +def assert_timestamp(ts: structs.Timestamp | None, repr: str, value: datetime) -> None: # noqa: A002 + """Test string conversion into a Timestamp dataclass.""" if not repr: assert ts is None else: @@ -39,8 +43,8 @@ def assert_timestamp(ts: structs.Timestamp, repr: str, value: datetime): assert ts.dt == value -def assert_code(code: structs.Code, repr: Any, value: Any): - """Tests string conversion into a conditional Code dataclass""" +def assert_code(code: structs.Code | None, repr: Any, value: Any) -> None: # noqa: A002 + """Test string conversion into a conditional Code dataclass.""" if not repr: assert code is None elif isinstance(code, str): @@ -51,16 +55,17 @@ def assert_code(code: structs.Code, repr: Any, value: Any): assert code.value == value -def assert_value(src: Optional[structs.Number], value: Union[int, float, None]): - """Tests a number's value matches the expected value while handling nulls""" +def assert_value(src: structs.Number | None, value: float | None) -> None: + """Test a number's value matches the expected value while handling nulls.""" if value is None: assert src is None else: + assert src is not None assert src.value == value -def get_data(filepath: str, report_type: str) -> Iterator[Tuple[dict, str, datetime]]: - """Returns a glob iterable of JSON files""" +def get_data(filepath: str, report_type: str) -> Iterator[tuple[dict, str, datetime]]: + """Return a glob iterable of JSON files.""" path = Path(filepath).parent.joinpath("data", report_type) for result in path.glob("*.json"): data = json.load(result.open(), object_hook=datetime_parser) @@ -70,24 +75,22 @@ def get_data(filepath: str, report_type: str) -> Iterator[Tuple[dict, str, datet def datetime_parser(data: dict) -> dict: - """Convert ISO strings into datetime objects""" + """Convert ISO strings into datetime objects.""" for key, val in data.items(): if isinstance(val, str) and len(val) > 6: if val[-3] == ":" and val[-6] in "+-": with suppress(ValueError): data[key] = datetime.fromisoformat(val) with suppress(ValueError): - data[key] = datetime.strptime(val, r"%Y-%m-%d") + data[key] = datetime.strptime(val, r"%Y-%m-%d").replace(tzinfo=timezone.utc) return data def round_coordinates(data: Any) -> Any: - """Recursively round lat,lon floats to 2 digits""" + """Recursively round lat,lon floats to 2 digits.""" if isinstance(data, dict): for key, val in data.items(): - data[key] = ( - round(val, 2) if key in {"lat", "lon"} else round_coordinates(val) - ) + data[key] = round(val, 2) if key in {"lat", "lon"} else round_coordinates(val) elif isinstance(data, list): data = [round_coordinates(i) for i in data] return data diff --git a/util/build_tests.py b/util/build_tests.py index ce4d42b..5b277f3 100644 --- a/util/build_tests.py +++ b/util/build_tests.py @@ -1,41 +1,40 @@ -""" -Creates files for end-to-end tests +"""Creates files for end-to-end tests.""" -python util/build_tests.py -""" +# ruff: noqa: INP001 # stdlib +from __future__ import annotations + import json -import random +import secrets import sys from dataclasses import asdict from datetime import date, datetime, timezone from pathlib import Path -from typing import Optional +from typing import Any sys.path.insert(0, str(Path(__file__).parent.parent.absolute())) # module -import avwx -from tests.util import round_coordinates from find_bad_stations import PROJECT_ROOT +import avwx +from tests.util import round_coordinates TESTS_PATH = PROJECT_ROOT / "tests" -def _default(o): - if isinstance(o, (date, datetime)): - return o.isoformat() +def _default(o: Any) -> str | None: + return o.isoformat() if isinstance(o, (date, datetime)) else None def save(data: dict, path: Path) -> None: - """Save JSON data to path""" + """Save JSON data to path.""" json.dump(data, path.open("w"), indent=4, sort_keys=True, default=_default) def make_metar_test(station: str) -> dict: - """Builds METAR test file for station""" + """Build METAR test file for station.""" m = avwx.Metar(station) m.update() return { @@ -47,7 +46,7 @@ def make_metar_test(station: str) -> dict: def make_taf_test(station: str) -> dict: - """Builds TAF test file for station""" + """Build TAF test file for station.""" t = avwx.Taf(station) t.update() return { @@ -59,8 +58,8 @@ def make_taf_test(station: str) -> dict: } -def make_pirep_test(station: str) -> Optional[dict]: - """Builds PIREP test file for station""" +def make_pirep_test(station: str) -> dict | None: + """Build PIREP test file for station.""" p = avwx.Pireps(station) p.update() if not p.data: @@ -69,8 +68,8 @@ def make_pirep_test(station: str) -> Optional[dict]: return {"reports": ret, "station": asdict(p.station)} -def make_notam_test(station: str) -> Optional[dict]: - """Builds NOTAM test file for station""" +def make_notam_test(station: str) -> dict | None: + """Build NOTAM test file for station.""" n = avwx.Notams(station) n.update() ret = [] @@ -82,47 +81,45 @@ def make_notam_test(station: str) -> Optional[dict]: return {"reports": ret, "station": asdict(n.station)} -def make_forecast_test( - report: avwx.forecast.base.Forecast, station: str -) -> Optional[dict]: - """Builds GFS service test file for station""" +def make_forecast_test(report: avwx.forecast.base.Forecast, station: str) -> dict | None: + """Build GFS service test file for station.""" g = report(station) g.update() return {"data": asdict(g.data), "station": asdict(g.station)} if g.data else None -# def make_mav_test(station: str) -> Optional[dict]: -# """Builds MAV test file for station""" +# def make_mav_test(station: str) -> dict | None: +# """Build MAV test file for station.""" # return make_forecast_test(avwx.Mav, station) -# def make_mex_test(station: str) -> Optional[dict]: -# """Builds MEX test file for station""" +# def make_mex_test(station: str) -> dict | None: +# """Build MEX test file for station.""" # return make_forecast_test(avwx.Mex, station) -def make_nbh_test(station: str) -> Optional[dict]: - """Builds NBH test file for station""" +def make_nbh_test(station: str) -> dict | None: + """Build NBH test file for station.""" return make_forecast_test(avwx.Nbh, station) -def make_nbs_test(station: str) -> Optional[dict]: - """Builds NBS test file for station""" +def make_nbs_test(station: str) -> dict | None: + """Build NBS test file for station.""" return make_forecast_test(avwx.Nbs, station) -def make_nbe_test(station: str) -> Optional[dict]: - """Builds NBE test file for station""" +def make_nbe_test(station: str) -> dict | None: + """Build NBE test file for station.""" return make_forecast_test(avwx.Nbe, station) -def make_nbx_test(station: str) -> Optional[dict]: - """Builds NBX test file for station""" +def make_nbx_test(station: str) -> dict | None: + """Build NBX test file for station.""" return make_forecast_test(avwx.Nbx, station) def make_airsigmet_tests() -> None: - """Builds IRMET/SIGMET test file""" + """Build IRMET/SIGMET test file.""" a = avwx.AirSigManager() a.update() if a.reports is None: @@ -130,7 +127,7 @@ def make_airsigmet_tests() -> None: reports = {} for _ in range(10): while True: - report = random.choice(a.reports) + report = secrets.choice(a.reports) key = report.raw[:20] if key not in reports: reports[key] = { @@ -142,8 +139,8 @@ def make_airsigmet_tests() -> None: save(list(reports.values()), path) -def main(): - """Creates source files for end-to-end tests""" +def main() -> None: + """Create source files for end-to-end tests.""" targets = { "current": ("metar", "taf", "pirep", "notam"), "forecast": ("nbh", "nbs", "nbe", "nbx"), @@ -155,9 +152,7 @@ def main(): if data := globals()[f"make_{report_type}_test"](icao): data["icao"] = icao data["created"] = datetime.now(tz=timezone.utc).date() - path = TESTS_PATH.joinpath( - target, "data", report_type, f"{icao}.json" - ) + path = TESTS_PATH.joinpath(target, "data", report_type, f"{icao}.json") save(data, path) make_airsigmet_tests() diff --git a/util/find_bad_stations.py b/util/find_bad_stations.py index 7efa726..6bb82c4 100644 --- a/util/find_bad_stations.py +++ b/util/find_bad_stations.py @@ -1,43 +1,40 @@ -""" -Manages good/bad station lists by calling METARs -""" +"""Manages good/bad station lists by calling METARs.""" -# pylint: disable=broad-except +# ruff: noqa: INP001,T201,BLE001 # stdlib -import random import asyncio as aio +import random from contextlib import suppress -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path # library -from kewkew import Kew +from kewkew import Kew # type: ignore # module import avwx -from avwx.service.bulk import NOAA_Bulk -from avwx.service.scrape import NOAA - +from avwx.service.bulk import NoaaBulk +from avwx.service.scrape import Noaa PROJECT_ROOT = Path(__file__).parent.parent GOOD_PATH = PROJECT_ROOT / "avwx" / "data" / "files" / "good_stations.txt" def load_stations(path: Path) -> set[str]: - """Load a station set from a path""" + """Load a station set from a path.""" return set(path.read_text().strip().split("\n")) -def save_stations(data: set[str], path: Path): - """Save a sation set to a path""" +def save_stations(data: set[str], path: Path) -> None: + """Save a sation set to a path.""" path.write_text("\n".join(sorted(data))) -async def get_noaa_codes() -> {str}: - """Create a set of current NOAA codes from bulk access""" +async def get_noaa_codes() -> set[str]: + """Create a set of current NOAA codes from bulk access.""" codes = set() - for report in await NOAA_Bulk("metar").async_fetch(): + for report in await NoaaBulk("metar").async_fetch(): items = report.strip(" '\"").split() if items[0] == "METAR": items.pop(0) @@ -46,7 +43,7 @@ async def get_noaa_codes() -> {str}: class StationTester(Kew): - """Station reporting queue manager""" + """Station reporting queue manager.""" good_stations: set[str] sleep_chance: float @@ -57,17 +54,16 @@ def __init__(self, stations: set[str], workers: int = 3) -> None: self.sleep_chance = 0.0 def should_test(self, code: str) -> bool: - """Returns False if an ident is known good or never good""" + """Return False if an ident is known good or never good.""" if code in self.good_stations: return False return not any(char.isdigit() for char in code) - async def worker(self, data: object) -> bool: - """Worker to check queued idents and update lists""" - code = data + async def worker(self, code: str) -> bool: + """Worker to check queued idents and update lists.""" try: metar = avwx.Metar(code) - if isinstance(metar.service, NOAA): # Skip NOAA if not in bulk pull + if isinstance(metar.service, Noaa): # Skip NOAA if not in bulk pull return True if await metar.async_update(): self.good_stations.add(code) @@ -79,13 +75,13 @@ async def worker(self, data: object) -> bool: print("\n", code, exc, "\n") return True - async def wait(self): - """Waits until the queue is empty""" + async def wait(self) -> None: + """Wait until the queue is empty.""" while not self._queue.empty(): await aio.sleep(0.01) - async def add_stations(self): - """Populate and run ICAO check queue""" + async def add_stations(self) -> None: + """Populate and run ICAO check queue.""" stations = [] for station in avwx.station.meta.STATIONS.values(): code = station["icao"] or station["gps"] @@ -97,16 +93,16 @@ async def add_stations(self): async def main() -> int: - """Update ICAO lists with 1 hour sleep cycle""" + """Update ICAO lists with 1 hour sleep cycle.""" tester = StationTester(load_stations(GOOD_PATH).union(await get_noaa_codes())) try: while True: - print("\nStarting", datetime.now()) + print("\nStarting", datetime.now(tz=UTC)) await tester.add_stations() await tester.wait() save_stations(tester.good_stations, GOOD_PATH) print(f"Good stations: {len(tester.good_stations)}") - print("Sleeping", datetime.now()) + print("Sleeping", datetime.now(tz=UTC)) await aio.sleep(60 * 60) except KeyboardInterrupt: pass diff --git a/util/update_codes.py b/util/update_codes.py index 26d5def..e67c585 100644 --- a/util/update_codes.py +++ b/util/update_codes.py @@ -1,14 +1,16 @@ -""" -Update ICAO lists from 3rd-party source -""" +"""Update ICAO lists from 3rd-party source.""" + +# ruff: noqa: INP001 # stdlib +from __future__ import annotations + +import asyncio as aio import gzip import json -import asyncio as aio from contextlib import suppress -from pathlib import Path from os import environ +from typing import TYPE_CHECKING # library import httpx @@ -17,6 +19,9 @@ # module from find_bad_stations import PROJECT_ROOT +if TYPE_CHECKING: + from pathlib import Path + load_dotenv() DATA_PATH = PROJECT_ROOT / "data" @@ -27,9 +32,10 @@ async def fetch_data( url: str, params: dict | None = None, headers: dict | None = None, + *, decode: bool = False, ) -> dict: - """Fetch source data with optional gzip decompress""" + """Fetch source data with optional gzip decompress.""" async with httpx.AsyncClient(timeout=TIMEOUT) as client: resp = await client.get( url, @@ -43,13 +49,13 @@ async def fetch_data( def save(data: list[str], path: Path) -> None: - """Export JSON data""" + """Export JSON data.""" data.sort() json.dump(data, path.open("w"), indent=2) async def update_icaos() -> None: - """Update ICAO list from Avio source""" + """Update ICAO list from Avio source.""" icaos = [] for station in await fetch_data( "https://api.aviowiki.com/export/airports", @@ -60,19 +66,19 @@ async def update_icaos() -> None: decode=True, ): if icao := station.get("icao"): - icaos.append(icao) + icaos.append(icao) # noqa: PERF401 save(icaos, DATA_PATH / "icaos.json") async def update_awos() -> None: - """Update AWOS code list""" + """Update AWOS code list.""" fetch = fetch_data(environ["AWOS_URL"], params={"authKey": environ["AWOS_KEY"]}) codes = [report.strip().split()[0] for report in await fetch] save(codes, DATA_PATH / "awos.json") async def main() -> None: - """Update static repo data codes""" + """Update static repo data codes.""" await aio.gather(update_icaos(), update_awos())