Merge pull request #154 from jepler/small-tweaks

This commit is contained in:
Jeff Epler 2025-08-27 09:06:49 -05:00 committed by GitHub
commit a416d2e760
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 72 additions and 39 deletions

View file

@ -55,10 +55,17 @@ jobs:
python -mpip install wheel
python -mpip install -r requirements-dev.txt
- name: Check stubs
- name: Check stubs with mypy
if: (! startsWith(matrix.python-version, 'pypy-'))
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:
strategy:

View file

@ -24,7 +24,13 @@ repos:
rev: v0.12.10
hooks:
# Run the linter.
- id: ruff
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- 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!

View file

@ -24,7 +24,7 @@ ENVPYTHON ?= _env/bin/python3
endif
.PHONY: default
default: coverage mypy
default: coverage mypy pyright pyrefly
COVERAGE_INCLUDE=--include "src/**/*.py"
.PHONY: coverage

View file

@ -7,6 +7,8 @@ build
click
coverage >= 7.10.3
mypy; implementation_name=="cpython"
pyright; implementation_name=="cpython"
pyrefly; implementation_name=="cpython"
click>=8.1.5; implementation_name=="cpython"
leapseconddata
platformdirs

View file

@ -20,15 +20,30 @@ import enum
import json
import warnings
from dataclasses import dataclass
from typing import ClassVar
from typing import ClassVar, Literal
from . import iersdata
from .tz import Mountain
WWVBChannel = Literal["amplitude", "phase", "both"]
TYPE_CHECKING = False
if TYPE_CHECKING:
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")
@ -927,7 +942,7 @@ styles = {
def print_timecodes(
w: WWVBMinute,
minutes: int,
channel: str,
channel: WWVBChannel,
style: str,
file: TextIO,
*,
@ -964,7 +979,7 @@ def print_timecodes(
def print_timecodes_json(
w: WWVBMinute,
minutes: int,
channel: str,
channel: WWVBChannel,
file: TextIO,
) -> None:
"""Print a range of timecodes in JSON format.
@ -984,7 +999,7 @@ def print_timecodes_json(
"""
result = []
for _ in range(minutes):
data: dict[str, Any] = {
data: JsonMinute = {
"year": w.year,
"days": w.days,
"hour": w.hour,

View file

@ -6,10 +6,10 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
import wwvb
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator

View file

@ -9,15 +9,18 @@ from __future__ import annotations
import datetime
import sys
from typing import Any
import click
import dateutil.parser
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"""
try:
if len(value) == 5:
@ -95,7 +98,7 @@ def main(
dut1: int,
minutes: int,
style: str,
channel: str,
channel: WWVBChannel,
all_timecodes: bool,
timespec: datetime.datetime,
) -> None:

View file

@ -8,13 +8,13 @@ from __future__ import annotations
import datetime
import functools
from tkinter import Canvas, TclError, Tk
from typing import TYPE_CHECKING, Any
from tkinter import Canvas, Event, TclError, Tk
import click
import wwvb
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Generator
@ -25,7 +25,7 @@ def _app() -> 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"""
app = _app()
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)
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"""
sz = min(event.width, event.height) - 8
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__
# pyrefly: ignore # bad-assignment
def after_func() -> None:
"""Repeatedly run the controller after the desired interval"""
app.after(controller(), after_func)
# pyrefly: ignore # bad-argument-type
app.after_idle(after_func)
app.mainloop()

View file

@ -1,4 +1,5 @@
#!/usr/bin/python3
"""Test most wwvblib commandline programs"""
# ruff: noqa: N802 D102
@ -6,13 +7,13 @@
#
# SPDX-License-Identifier: GPL-3.0-only
from __future__ import annotations
import json
import os
import subprocess
import sys
import unittest
from collections.abc import Sequence
from typing import Any
# These imports must remain, even though the module contents are not used directly!
import wwvb.dut1table
@ -22,6 +23,12 @@ import wwvb.gen
assert wwvb.dut1table.__name__ == "wwvb.dut1table"
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):
"""Test various CLI commands within wwvbpy"""
@ -55,9 +62,10 @@ class CLITestCase(unittest.TestCase):
def assertStarts(self, expected: str, actual: str, *args: str) -> None:
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"""
actual = self.moduleOutput(*args)
# Note: in mypy, revealed type of json.loads is typing.Any!
self.assertEqual(json.loads(actual), expected)
def assertModuleOutputStarts(self, expected: str, *args: str) -> None:

View file

@ -56,7 +56,3 @@ class TestLeapSecond(unittest.TestCase):
assert not our_is_ls
d = datetime.datetime.combine(nm, datetime.time(), tzinfo=datetime.timezone.utc)
self.assertEqual(leap, bench)
if __name__ == "__main__":
unittest.main()

View file

@ -27,7 +27,3 @@ class TestPhaseModulation(unittest.TestCase):
self.assertEqual(ref_am, test_am)
self.assertEqual(ref_pm, test_pm)
if __name__ == "__main__":
unittest.main()

View file

@ -47,7 +47,7 @@ class WWVBRoundtrip(unittest.TestCase):
any_leap_second = False
for _ in range(20):
timecode = minute.as_timecode()
decoded = None
decoded: uwwvb.WWVBMinute | None = None
if len(timecode.am) == 61:
any_leap_second = True
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!
uwwvb.as_datetime_local(decoded),
)
if __name__ == "__main__":
unittest.main()

View file

@ -14,11 +14,12 @@ import io
import pathlib
import random
import sys
import typing
import unittest
import uwwvb
import wwvb
from wwvb import decode, iersdata, tz
from wwvb import WWVBChannel, decode, iersdata, tz
class WWVBMinute2k(wwvb.WWVBMinute):
@ -44,11 +45,16 @@ class WWVBTestCase(unittest.TestCase):
header = lines[0].split()
timestamp = " ".join(header[:10])
options = header[10:]
channel = "amplitude"
channel: WWVBChannel = "amplitude"
style = "default"
for o in options:
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="):
style = o[8:]
else:
@ -430,7 +436,3 @@ class WWVBRoundtrip(unittest.TestCase):
minute.am[57] = wwvb.AmplitudeModulation.MARK
decoded_minute = wwvb.WWVBMinute.from_timecode_am(minute)
assert decoded_minute is None
if __name__ == "__main__":
unittest.main()