Merge pull request #154 from jepler/small-tweaks
This commit is contained in:
commit
a416d2e760
13 changed files with 72 additions and 39 deletions
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
|
|
@ -55,10 +55,17 @@ jobs:
|
||||||
python -mpip install wheel
|
python -mpip install wheel
|
||||||
python -mpip install -r requirements-dev.txt
|
python -mpip install -r requirements-dev.txt
|
||||||
|
|
||||||
- name: Check stubs
|
- name: Check stubs with mypy
|
||||||
if: (! startsWith(matrix.python-version, 'pypy-'))
|
if: (! startsWith(matrix.python-version, 'pypy-'))
|
||||||
run: make mypy PYTHON=python
|
run: make mypy PYTHON=python
|
||||||
|
|
||||||
|
- name: Check stubs with pyrefly
|
||||||
|
if: (! startsWith(matrix.python-version, 'pypy-'))
|
||||||
|
run: make pyrefly PYTHON=python
|
||||||
|
|
||||||
|
- name: Check stubs with pyright
|
||||||
|
if: (! startsWith(matrix.python-version, 'pypy-'))
|
||||||
|
run: make pyright PYTHON=python
|
||||||
|
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,13 @@ repos:
|
||||||
rev: v0.12.10
|
rev: v0.12.10
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff-check
|
||||||
args: [ --fix ]
|
args: [ --fix ]
|
||||||
# Run the formatter.
|
# Run the formatter.
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.20.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [ --py39-plus ]
|
||||||
|
exclude: src/uwwvb.py # CircuitPython prevaling standard!
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -24,7 +24,7 @@ ENVPYTHON ?= _env/bin/python3
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: default
|
.PHONY: default
|
||||||
default: coverage mypy
|
default: coverage mypy pyright pyrefly
|
||||||
|
|
||||||
COVERAGE_INCLUDE=--include "src/**/*.py"
|
COVERAGE_INCLUDE=--include "src/**/*.py"
|
||||||
.PHONY: coverage
|
.PHONY: coverage
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ build
|
||||||
click
|
click
|
||||||
coverage >= 7.10.3
|
coverage >= 7.10.3
|
||||||
mypy; implementation_name=="cpython"
|
mypy; implementation_name=="cpython"
|
||||||
|
pyright; implementation_name=="cpython"
|
||||||
|
pyrefly; implementation_name=="cpython"
|
||||||
click>=8.1.5; implementation_name=="cpython"
|
click>=8.1.5; implementation_name=="cpython"
|
||||||
leapseconddata
|
leapseconddata
|
||||||
platformdirs
|
platformdirs
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,30 @@ import enum
|
||||||
import json
|
import json
|
||||||
import warnings
|
import warnings
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import ClassVar
|
from typing import ClassVar, Literal
|
||||||
|
|
||||||
from . import iersdata
|
from . import iersdata
|
||||||
from .tz import Mountain
|
from .tz import Mountain
|
||||||
|
|
||||||
|
WWVBChannel = Literal["amplitude", "phase", "both"]
|
||||||
|
|
||||||
TYPE_CHECKING = False
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from typing import Any, Self, TextIO, TypeVar
|
from typing import NotRequired, Self, TextIO, TypedDict, TypeVar
|
||||||
|
|
||||||
|
class JsonMinute(TypedDict):
|
||||||
|
"""Implementation detail
|
||||||
|
|
||||||
|
This is the Python object type that is serialized by `print_timecodes_json`
|
||||||
|
"""
|
||||||
|
|
||||||
|
year: int
|
||||||
|
days: int
|
||||||
|
hour: int
|
||||||
|
minute: int
|
||||||
|
amplitude: NotRequired[str]
|
||||||
|
phase: NotRequired[str]
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
@ -927,7 +942,7 @@ styles = {
|
||||||
def print_timecodes(
|
def print_timecodes(
|
||||||
w: WWVBMinute,
|
w: WWVBMinute,
|
||||||
minutes: int,
|
minutes: int,
|
||||||
channel: str,
|
channel: WWVBChannel,
|
||||||
style: str,
|
style: str,
|
||||||
file: TextIO,
|
file: TextIO,
|
||||||
*,
|
*,
|
||||||
|
|
@ -964,7 +979,7 @@ def print_timecodes(
|
||||||
def print_timecodes_json(
|
def print_timecodes_json(
|
||||||
w: WWVBMinute,
|
w: WWVBMinute,
|
||||||
minutes: int,
|
minutes: int,
|
||||||
channel: str,
|
channel: WWVBChannel,
|
||||||
file: TextIO,
|
file: TextIO,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Print a range of timecodes in JSON format.
|
"""Print a range of timecodes in JSON format.
|
||||||
|
|
@ -984,7 +999,7 @@ def print_timecodes_json(
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for _ in range(minutes):
|
for _ in range(minutes):
|
||||||
data: dict[str, Any] = {
|
data: JsonMinute = {
|
||||||
"year": w.year,
|
"year": w.year,
|
||||||
"days": w.days,
|
"days": w.days,
|
||||||
"hour": w.hour,
|
"hour": w.hour,
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
import wwvb
|
import wwvb
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import dateutil.parser
|
import dateutil.parser
|
||||||
|
|
||||||
from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
|
from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import WWVBChannel
|
||||||
|
|
||||||
def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
|
||||||
|
def parse_timespec(ctx: click.Context, param: click.Parameter, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
||||||
"""Parse a time specifier from the commandline"""
|
"""Parse a time specifier from the commandline"""
|
||||||
try:
|
try:
|
||||||
if len(value) == 5:
|
if len(value) == 5:
|
||||||
|
|
@ -95,7 +98,7 @@ def main(
|
||||||
dut1: int,
|
dut1: int,
|
||||||
minutes: int,
|
minutes: int,
|
||||||
style: str,
|
style: str,
|
||||||
channel: str,
|
channel: WWVBChannel,
|
||||||
all_timecodes: bool,
|
all_timecodes: bool,
|
||||||
timespec: datetime.datetime,
|
timespec: datetime.datetime,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
from tkinter import Canvas, TclError, Tk
|
from tkinter import Canvas, Event, TclError, Tk
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import wwvb
|
import wwvb
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ def _app() -> Tk:
|
||||||
return Tk()
|
return Tk()
|
||||||
|
|
||||||
|
|
||||||
def validate_colors(ctx: Any, param: Any, value: str) -> list[str]: # noqa: ARG001
|
def validate_colors(ctx: click.Context, param: click.Parameter, value: str) -> list[str]: # noqa: ARG001
|
||||||
"""Check that all colors in a string are valid, splitting it to a list"""
|
"""Check that all colors in a string are valid, splitting it to a list"""
|
||||||
app = _app()
|
app = _app()
|
||||||
colors = value.split()
|
colors = value.split()
|
||||||
|
|
@ -106,7 +106,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
||||||
canvas.pack(fill="both", expand=True)
|
canvas.pack(fill="both", expand=True)
|
||||||
app.wm_deiconify()
|
app.wm_deiconify()
|
||||||
|
|
||||||
def resize_canvas(event: Any) -> None:
|
def resize_canvas(event: Event) -> None:
|
||||||
"""Keep the circle filling the window when it is resized"""
|
"""Keep the circle filling the window when it is resized"""
|
||||||
sz = min(event.width, event.height) - 8
|
sz = min(event.width, event.height) - 8
|
||||||
if sz < 0:
|
if sz < 0:
|
||||||
|
|
@ -141,10 +141,12 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
|
||||||
|
|
||||||
controller = controller_func().__next__
|
controller = controller_func().__next__
|
||||||
|
|
||||||
|
# pyrefly: ignore # bad-assignment
|
||||||
def after_func() -> None:
|
def after_func() -> None:
|
||||||
"""Repeatedly run the controller after the desired interval"""
|
"""Repeatedly run the controller after the desired interval"""
|
||||||
app.after(controller(), after_func)
|
app.after(controller(), after_func)
|
||||||
|
|
||||||
|
# pyrefly: ignore # bad-argument-type
|
||||||
app.after_idle(after_func)
|
app.after_idle(after_func)
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
"""Test most wwvblib commandline programs"""
|
"""Test most wwvblib commandline programs"""
|
||||||
|
|
||||||
# ruff: noqa: N802 D102
|
# ruff: noqa: N802 D102
|
||||||
|
|
@ -6,13 +7,13 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from collections.abc import Sequence
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
# These imports must remain, even though the module contents are not used directly!
|
# These imports must remain, even though the module contents are not used directly!
|
||||||
import wwvb.dut1table
|
import wwvb.dut1table
|
||||||
|
|
@ -22,6 +23,12 @@ import wwvb.gen
|
||||||
assert wwvb.dut1table.__name__ == "wwvb.dut1table"
|
assert wwvb.dut1table.__name__ == "wwvb.dut1table"
|
||||||
assert wwvb.gen.__name__ == "wwvb.gen"
|
assert wwvb.gen.__name__ == "wwvb.gen"
|
||||||
|
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from wwvb import JsonMinute
|
||||||
|
|
||||||
|
|
||||||
class CLITestCase(unittest.TestCase):
|
class CLITestCase(unittest.TestCase):
|
||||||
"""Test various CLI commands within wwvbpy"""
|
"""Test various CLI commands within wwvbpy"""
|
||||||
|
|
@ -55,9 +62,10 @@ class CLITestCase(unittest.TestCase):
|
||||||
def assertStarts(self, expected: str, actual: str, *args: str) -> None:
|
def assertStarts(self, expected: str, actual: str, *args: str) -> None:
|
||||||
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
|
self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}")
|
||||||
|
|
||||||
def assertModuleJson(self, expected: Any, *args: str) -> None:
|
def assertModuleJson(self, expected: list[JsonMinute], *args: str) -> None:
|
||||||
"""Check the output from invoking a `python -m modulename` program matches the expected"""
|
"""Check the output from invoking a `python -m modulename` program matches the expected"""
|
||||||
actual = self.moduleOutput(*args)
|
actual = self.moduleOutput(*args)
|
||||||
|
# Note: in mypy, revealed type of json.loads is typing.Any!
|
||||||
self.assertEqual(json.loads(actual), expected)
|
self.assertEqual(json.loads(actual), expected)
|
||||||
|
|
||||||
def assertModuleOutputStarts(self, expected: str, *args: str) -> None:
|
def assertModuleOutputStarts(self, expected: str, *args: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,3 @@ class TestLeapSecond(unittest.TestCase):
|
||||||
assert not our_is_ls
|
assert not our_is_ls
|
||||||
d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
|
d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
|
||||||
self.assertEqual(leap, bench)
|
self.assertEqual(leap, bench)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,3 @@ class TestPhaseModulation(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(ref_am, test_am)
|
self.assertEqual(ref_am, test_am)
|
||||||
self.assertEqual(ref_pm, test_pm)
|
self.assertEqual(ref_pm, test_pm)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
||||||
any_leap_second = False
|
any_leap_second = False
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
timecode = minute.as_timecode()
|
timecode = minute.as_timecode()
|
||||||
decoded = None
|
decoded: uwwvb.WWVBMinute | None = None
|
||||||
if len(timecode.am) == 61:
|
if len(timecode.am) == 61:
|
||||||
any_leap_second = True
|
any_leap_second = True
|
||||||
for code in timecode.am:
|
for code in timecode.am:
|
||||||
|
|
@ -215,7 +215,3 @@ class WWVBRoundtrip(unittest.TestCase):
|
||||||
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
|
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
|
||||||
uwwvb.as_datetime_local(decoded),
|
uwwvb.as_datetime_local(decoded),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@ import io
|
||||||
import pathlib
|
import pathlib
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import uwwvb
|
import uwwvb
|
||||||
import wwvb
|
import wwvb
|
||||||
from wwvb import decode, iersdata, tz
|
from wwvb import WWVBChannel, decode, iersdata, tz
|
||||||
|
|
||||||
|
|
||||||
class WWVBMinute2k(wwvb.WWVBMinute):
|
class WWVBMinute2k(wwvb.WWVBMinute):
|
||||||
|
|
@ -44,11 +45,16 @@ class WWVBTestCase(unittest.TestCase):
|
||||||
header = lines[0].split()
|
header = lines[0].split()
|
||||||
timestamp = " ".join(header[:10])
|
timestamp = " ".join(header[:10])
|
||||||
options = header[10:]
|
options = header[10:]
|
||||||
channel = "amplitude"
|
channel: WWVBChannel = "amplitude"
|
||||||
style = "default"
|
style = "default"
|
||||||
for o in options:
|
for o in options:
|
||||||
if o.startswith("--channel="):
|
if o.startswith("--channel="):
|
||||||
channel = o[10:]
|
value = o[10:]
|
||||||
|
if value in {"both", "amplitude", "phase"}:
|
||||||
|
# pyrefly: ignore # redundant-cast
|
||||||
|
channel = typing.cast("WWVBChannel", value)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown channel {o!r}")
|
||||||
elif o.startswith("--style="):
|
elif o.startswith("--style="):
|
||||||
style = o[8:]
|
style = o[8:]
|
||||||
else:
|
else:
|
||||||
|
|
@ -430,7 +436,3 @@ class WWVBRoundtrip(unittest.TestCase):
|
||||||
minute.am[57] = wwvb.AmplitudeModulation.MARK
|
minute.am[57] = wwvb.AmplitudeModulation.MARK
|
||||||
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
|
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
|
||||||
assert decoded_minute is None
|
assert decoded_minute is None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue