Compare commits

..

1 commit

Author SHA1 Message Date
pre-commit-ci[bot]
f36fa32a81
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.12.4 → v0.12.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.4...v0.12.5)
2025-07-28 20:41:40 +00:00
19 changed files with 97 additions and 161 deletions

View file

@ -55,17 +55,10 @@ jobs:
python -mpip install wheel
python -mpip install -r requirements-dev.txt
- name: Check stubs with mypy
- name: Check stubs
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

@ -8,7 +8,7 @@ default_language_version:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v5.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
@ -21,16 +21,10 @@ repos:
- id: reuse
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.10
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff-check
- id: ruff
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,13 +24,13 @@ ENVPYTHON ?= _env/bin/python3
endif
.PHONY: default
default: coverage mypy pyright pyrefly
default: coverage mypy
COVERAGE_INCLUDE=--include "src/**/*.py"
.PHONY: coverage
coverage:
$(Q)$(PYTHON) -mcoverage erase
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run -p -m unittest discover -s test
$(Q)env PYTHONPATH=src $(PYTHON) -mcoverage run --branch -p -m unittest discover -s test
$(Q)$(PYTHON) -mcoverage combine -q
$(Q)$(PYTHON) -mcoverage html $(COVERAGE_INCLUDE)
$(Q)$(PYTHON) -mcoverage xml $(COVERAGE_INCLUDE)
@ -46,15 +46,6 @@ test_venv:
mypy:
$(Q)mypy --strict --no-warn-unused-ignores src test
.PHONY: pyright
pyright:
$(Q)pyright src test
.PHONY: pyrefly
pyrefly:
$(Q)pyrefly check src test
.PHONY: update
update:
$(Q)env PYTHONPATH=src $(PYTHON) -mwwvb.updateiers --dist

View file

@ -4,6 +4,7 @@ SPDX-FileCopyrightText: 2021-2024 Jeff Epler
SPDX-License-Identifier: GPL-3.0-only
-->
[![Test wwvbgen](https://github.com/jepler/wwvbpy/actions/workflows/test.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/jepler/wwvbpy/branch/main/graph/badge.svg?token=Exx0c3Gp65)](https://codecov.io/gh/jepler/wwvbpy)
[![Update DUT1 data](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml/badge.svg)](https://github.com/jepler/wwvbpy/actions/workflows/cron.yml)
[![PyPI](https://img.shields.io/pypi/v/wwvb)](https://pypi.org/project/wwvb)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jepler/wwvbpy/main.svg)](https://results.pre-commit.ci/latest/github/jepler/wwvbpy/main)

0
codecov.yml Normal file
View file

View file

@ -51,6 +51,5 @@ wwvbtk = "wwvb.wwvbtk:main"
[[tool.mypy.overrides]]
module = ["adafruit_datetime"]
follow_untyped_imports = true
[tool.coverage.run]
patch=["subprocess"]
branch=true
[tool.coverage.report]
exclude_also=["if TYPE_CHECKING:"]

View file

@ -5,10 +5,8 @@ adafruit-circuitpython-datetime
beautifulsoup4
build
click
coverage >= 7.10.3
coverage >= 7.1.0
mypy; implementation_name=="cpython"
pyright; implementation_name=="cpython"
pyrefly; implementation_name=="cpython"
click>=8.1.5; implementation_name=="cpython"
leapseconddata
platformdirs

View file

@ -19,36 +19,25 @@ import datetime
import enum
import json
import warnings
from dataclasses import dataclass
from typing import ClassVar, Literal
from typing import TYPE_CHECKING, Any, NamedTuple, TextIO, TypeVar
from typing_extensions import Self
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 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")
HOUR = datetime.timedelta(seconds=3600)
SECOND = datetime.timedelta(seconds=1)
T = TypeVar("T")
def _removeprefix(s: str, p: str) -> str:
if s.startswith(p):
return s[len(p) :]
return s
def _date(dt: datetime.date) -> datetime.date:
@ -352,13 +341,8 @@ class DstStatus(enum.IntEnum):
"""DST in effect all day today"""
@dataclass(frozen=True)
class WWVBMinute:
"""Uniquely identifies a minute of time in the WWVB system.
To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
object instead.
"""
class _WWVBMinute(NamedTuple):
"""(implementation detail)"""
year: int
"""2-digit year within the WWVB epoch"""
@ -369,7 +353,7 @@ class WWVBMinute:
hour: int
"""UTC hour of day"""
minute: int
min: int
"""Minute of hour"""
dst: DstStatus
@ -384,10 +368,18 @@ class WWVBMinute:
ly: bool
"""Leap year flag"""
epoch: ClassVar[int] = 1970
def __init__(
self,
class WWVBMinute(_WWVBMinute):
"""Uniquely identifies a minute of time in the WWVB system.
To use ``ut1`` and ``ls`` information from IERS, create a `WWVBMinuteIERS`
object instead.
"""
epoch: int = 1970
def __new__(
cls,
year: int,
days: int,
hour: int,
@ -397,7 +389,7 @@ class WWVBMinute:
*,
ls: bool | None = None,
ly: bool | None = None,
) -> None:
) -> Self:
"""Construct a WWVBMinute
:param year: The 2- or 4-digit year. This parameter is converted by the `full_year` method.
@ -411,23 +403,15 @@ class WWVBMinute:
:param ls: Leap second warning flag
:param ly: Leap year flag
"""
dst = self.get_dst(year, days) if dst is None else DstStatus(dst)
dst = cls.get_dst(year, days) if dst is None else DstStatus(dst)
if ut1 is None and ls is None:
ut1, ls = self._get_dut1_info(year, days)
ut1, ls = cls._get_dut1_info(year, days)
elif ut1 is None or ls is None:
raise ValueError("sepecify both ut1 and ls or neither one")
year = self.full_year(year)
year = cls.full_year(year)
if ly is None:
ly = isly(year)
super().__setattr__("year", year)
super().__setattr__("days", days)
super().__setattr__("hour", hour)
super().__setattr__("minute", minute)
super().__setattr__("dst", dst)
super().__setattr__("ut1", ut1)
super().__setattr__("ls", ls)
super().__setattr__("ly", ly)
return _WWVBMinute.__new__(cls, year, days, hour, minute, dst, ut1, ls, ly)
@classmethod
def full_year(cls, year: int) -> int:
@ -461,7 +445,7 @@ class WWVBMinute:
"""Implement str()"""
return (
f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} "
f"min={self.minute:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
f"min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
f"ls={int(self.ls)}"
)
@ -471,7 +455,7 @@ class WWVBMinute:
The returned object has ``tzinfo=datetime.timezone.utc``.
"""
d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.minute * 60)
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
return d
as_datetime = as_datetime_utc
@ -525,7 +509,7 @@ class WWVBMinute:
return 60
if not self._is_end_of_month():
return 60
if self.hour != 23 or self.minute != 59:
if self.hour != 23 or self.min != 59:
return 60
if self.ut1 > 0:
return 59
@ -569,7 +553,7 @@ class WWVBMinute:
t.am[60] = AmplitudeModulation.MARK
for i in [4, 10, 11, 14, 20, 21, 24, 34, 35, 44, 54]:
t.am[i] = AmplitudeModulation.ZERO
t._put_am_bcd(self.minute, 1, 2, 3, 5, 6, 7, 8)
t._put_am_bcd(self.min, 1, 2, 3, 5, 6, 7, 8)
t._put_am_bcd(self.hour, 12, 13, 15, 16, 17, 18)
t._put_am_bcd(self.days, 22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
ut1_sign = self.ut1 >= 0
@ -583,14 +567,14 @@ class WWVBMinute:
def _fill_pm_timecode_extended(self, t: WWVBTimecode) -> None:
"""During minutes 10..15 and 40..45, the amplitude signal holds 'extended information'"""
assert 10 <= self.minute < 16 or 40 <= self.minute < 46
minno = self.minute % 10
assert 10 <= self.min < 16 or 40 <= self.min < 46
minno = self.min % 10
assert minno < 6
dst = self.dst
# Note that these are 1 different than Table 11
# because our LFSR sequence is zero-based
seqno = (self.minute // 30) * 2
seqno = (self.min // 30) * 2
if dst == 0:
pass
elif dst == 3:
@ -673,17 +657,17 @@ class WWVBMinute:
def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
"""Fill the phase portion of a timecode object"""
if 10 <= self.minute < 16 or 40 <= self.minute < 46:
if 10 <= self.min < 16 or 40 <= self.min < 46:
self._fill_pm_timecode_extended(t)
else:
self._fill_pm_timecode_regular(t)
def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> Self:
def next_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
"""Return an object representing the next minute"""
d = self.as_datetime() + datetime.timedelta(minutes=1)
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> Self:
def previous_minute(self, *, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
"""Return an object representing the previous minute"""
d = self.as_datetime() - datetime.timedelta(minutes=1)
return self.from_datetime(d, newut1=newut1, newls=newls, old_time=self)
@ -702,9 +686,9 @@ class WWVBMinute:
return 0, False
@classmethod
def fromstring(cls, s: str) -> Self:
def fromstring(cls, s: str) -> WWVBMinute:
"""Construct a WWVBMinute from a string representation created by print_timecodes"""
s = s.removeprefix("WWVB timecode: ")
s = _removeprefix(s, "WWVB timecode: ")
d: dict[str, int] = {}
for part in s.split():
k, v = part.split("=")
@ -718,7 +702,7 @@ class WWVBMinute:
dst = d.pop("dst", None)
ut1 = d.pop("ut1", None)
ls = d.pop("ls", None)
d.pop("ly", None) # Always use calculated ly flag
d.pop("ly", None)
if d:
raise ValueError(f"Invalid options: {d}")
return cls(year, days, hour, minute, dst, ut1=ut1, ls=None if ls is None else bool(ls))
@ -731,7 +715,7 @@ class WWVBMinute:
newut1: int | None = None,
newls: bool | None = None,
old_time: WWVBMinute | None = None,
) -> Self:
) -> WWVBMinute:
"""Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
u = d.utctimetuple()
if newls is None and newut1 is None:
@ -739,7 +723,7 @@ class WWVBMinute:
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
@classmethod
def from_timecode_am(cls, t: WWVBTimecode) -> Self | None: # noqa: PLR0912
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None: # noqa: PLR0912
"""Construct a WWVBMinute from a WWVBTimecode"""
for i in (0, 9, 19, 29, 39, 49, 59):
if t.am[i] != AmplitudeModulation.MARK:
@ -782,15 +766,6 @@ class WWVBMinute:
return None
return cls(year, days, hour, minute, dst, ut1, ls=ls, ly=ly)
@property
def min(self) -> int:
"""Deprecated alias for `WWVBMinute.minute`
Update your code to use the `minute` property instead of the `min` property.
"""
warnings.warn("WWVBMinute.min property is deprecated", category=DeprecationWarning, stacklevel=1)
return self.minute
class WWVBMinuteIERS(WWVBMinute):
"""A WWVBMinute that uses a database of DUT1 information"""
@ -942,7 +917,7 @@ styles = {
def print_timecodes(
w: WWVBMinute,
minutes: int,
channel: WWVBChannel,
channel: str,
style: str,
file: TextIO,
*,
@ -961,7 +936,7 @@ def print_timecodes(
print(file=file)
print(f"WWVB timecode: {w!s}{channel_text}{style_text}", file=file)
first = False
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.minute:02d} "
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
tc = w.as_timecode()
if len(style_chars) == 6:
print(f"{pfx} {tc.to_both_string(style_chars)}", file=file)
@ -979,7 +954,7 @@ def print_timecodes(
def print_timecodes_json(
w: WWVBMinute,
minutes: int,
channel: WWVBChannel,
channel: str,
file: TextIO,
) -> None:
"""Print a range of timecodes in JSON format.
@ -999,11 +974,11 @@ def print_timecodes_json(
"""
result = []
for _ in range(minutes):
data: JsonMinute = {
data: dict[str, Any] = {
"year": w.year,
"days": w.days,
"hour": w.hour,
"minute": w.minute,
"minute": w.min,
}
tc = w.as_timecode()

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,18 +9,15 @@ 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: click.Context, param: click.Parameter, value: list[str]) -> datetime.datetime: # noqa: ARG001
def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
"""Parse a time specifier from the commandline"""
try:
if len(value) == 5:
@ -98,7 +95,7 @@ def main(
dut1: int,
minutes: int,
style: str,
channel: WWVBChannel,
channel: str,
all_timecodes: bool,
timespec: datetime.datetime,
) -> None:

View file

@ -1 +1 @@
{"START": "1972-01-01", "OFFSETS_GZ": "H4sIAOvijWgC/+2aa3LDMAiEL5uHLTuxnN5/pn/aTmfSSiAWhGy+E2SWZQE58zwiH/1YivB/96vMXiIX2Io8CTyIrDSWGqlMRdrpDa6aJFnr0m4wYZkCE2UmSF0V+13vBveStK6JTfQyW3O86HLJf0RvDgy5u4FCI+WVKTsVoUdHzsrRoWRfYHIItZ5EEgu0Beu58EgEpMpO9zf4/s3iNO4y7/hqEwOZIPu3+PuO2T7Ic5E8GxsnZHvUYOtELxW1WP+0yx/caFxpyAooq6lq06UEr+UkLeXOIDPZ6EBrqb5K8Tvu6/B9CdnZqFQL05s2KauWy/IeF/tJGAisjK9MgGyDuUkRq4G1gRE+VjA30uZNPsdantkgMq58QO4fw+sqzj+A2/16mmvnyy9UzDvMktDgKYlnkFeB2rx+wNANG40aA4OgsY03AWoDCVs/XMmkyQ0+0jWaUqPdwA0m/MRuccGjCwirHToWzbcs8P7U1nZZLSYdHapWu5HqVg1YjK2fPEwvPZPzLPUF848tyid2u7dh8B7h+wVQ923Q+kqxZe3JclSSB+YTM3nnHrjgFth/vzgZzw6cbOMYa4bHFPU/DR3mp/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V+zUyyvbb/gE8h8bi3oTQAA"}
{"START": "1972-01-01", "OFFSETS_GZ": "H4sIAIYEZWgC/+2aa3LDMAiEL5uHLTuxnN5/pn/aTmfSSiAWhGy+E2SWZQE58zwiH/1YivB/96vMXiIX2Io8CTyIrDSWGqlMRdrpDa6aJFnr0m4wYZkCE2UmSF0V+13vBveStK6JTfQyW3O86HLJf0RvDgy5u4FCI+WVKTsVoUdHzsrRoWRfYHIItZ5EEgu0Beu58EgEpMpO9zf4/s3iNO4y7/hqEwOZIPu3+PuO2T7Ic5E8GxsnZHvUYOtELxW1WP+0yx/caFxpyAooq6lq06UEr+UkLeXOIDPZ6EBrqb5K8Tvu6/B9CdnZqFQL05s2KauWy/IeF/tJGAisjK9MgGyDuUkRq4G1gRE+VjA30uZNPsdantkgMq58QO4fw+sqzj+A2/16mmvnyy9UzDvMktDgKYlnkFeB2rx+wNANG40aA4OgsY03AWoDCVs/XMmkyQ0+0jWaUqPdwA0m/MRuccGjCwirHToWzbcs8P7U1nZZLSYdHapWu5HqVg1YjK2fPEwvPZPzLPUF848tyid2u7dh8B7h+wVQ923Q+kqxZe3JclSSB+YTM3nnHrjgFth/vzgZzw6cbOMYa4bHFPU/DR3mp/ubKM4cgwMnHZW4GFxFprOVcevAKGva6oExn1MOmyGDJQPm0rpU8bjqdOo993O6Xz9ofToZela5vwrWoTn4l4o5CIIaKejCEgSnJv784V6y0/IJeROtycVNAAA="}

View file

@ -47,7 +47,6 @@ def update_iersdata( # noqa: PLR0915
"""Update iersdata.py"""
offsets: list[int] = []
iersdata_text = _get_text(IERS_URL)
table_start: datetime.date | None = None
for r in csv.DictReader(io.StringIO(iersdata_text), delimiter=";"):
jd = float(r["MJD"])
offs_str = r["UT1-UTC"]
@ -80,8 +79,6 @@ def update_iersdata( # noqa: PLR0915
offsets.append(offs)
assert table_start is not None
wwvb_text = _get_text(NIST_URL)
wwvb_data = bs4.BeautifulSoup(wwvb_text, features="html.parser")
wwvb_dut1_table = wwvb_data.findAll("table")[2]
@ -91,7 +88,6 @@ def update_iersdata( # noqa: PLR0915
wwvb_data_stamp = datetime.datetime.fromisoformat(meta.attrs["content"]).replace(tzinfo=None).date()
def patch(patch_start: datetime.date, patch_end: datetime.date, val: int) -> None:
assert table_start is not None
off_start = (patch_start - table_start).days
off_end = (patch_end - table_start).days
offsets[off_start:off_end] = [val] * (off_end - off_start)

View file

@ -8,13 +8,13 @@ from __future__ import annotations
import datetime
import functools
from tkinter import Canvas, Event, TclError, Tk
from tkinter import Canvas, TclError, Tk
from typing import TYPE_CHECKING, Any
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: click.Context, param: click.Parameter, value: str) -> list[str]: # noqa: ARG001
def validate_colors(ctx: Any, param: Any, 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()
@ -69,7 +69,7 @@ def main(colors: list[str], size: int, min_size: int | None) -> None: # noqa: P
def deadline_ms(deadline: datetime.datetime) -> int:
"""Compute the number of ms until a deadline"""
now = datetime.datetime.now(datetime.timezone.utc)
return int(max(0.0, (deadline - now).total_seconds()) * 1000)
return int(max(0, (deadline - now).total_seconds()) * 1000)
def wwvbtick() -> Generator[tuple[datetime.datetime, wwvb.AmplitudeModulation]]:
"""Yield consecutive values of the WWVB amplitude signal, going from minute to minute"""
@ -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: Event) -> None:
def resize_canvas(event: Any) -> None:
"""Keep the circle filling the window when it is resized"""
sz = min(event.width, event.height) - 8
if sz < 0:
@ -141,12 +141,10 @@ 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,5 +1,4 @@
#!/usr/bin/python3
"""Test most wwvblib commandline programs"""
# ruff: noqa: N802 D102
@ -7,13 +6,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
@ -23,11 +22,7 @@ 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
coverage_add = ("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else ()
class CLITestCase(unittest.TestCase):
@ -39,10 +34,10 @@ class CLITestCase(unittest.TestCase):
return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env)
def moduleArgs(self, *args: str) -> Sequence[str]:
return (sys.executable, "-m", *args)
return (sys.executable, *coverage_add, "-m", *args)
def moduleOutput(self, *args: str) -> str:
return self.programOutput(sys.executable, "-m", *args)
return self.programOutput(sys.executable, *coverage_add, "-m", *args)
def assertProgramOutput(self, expected: str, *args: str) -> None:
"""Check the output from invoking a program matches the expected"""
@ -62,10 +57,9 @@ 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: list[JsonMinute], *args: str) -> None:
def assertModuleJson(self, expected: Any, *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,3 +56,7 @@ 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,3 +27,7 @@ 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: uwwvb.WWVBMinute | None = None
decoded = None
if len(timecode.am) == 61:
any_leap_second = True
for code in timecode.am:
@ -215,3 +215,7 @@ 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

@ -18,7 +18,7 @@ import unittest
import uwwvb
import wwvb
from wwvb import WWVBChannel, decode, iersdata, tz
from wwvb import decode, iersdata, tz
class WWVBMinute2k(wwvb.WWVBMinute):
@ -44,15 +44,11 @@ class WWVBTestCase(unittest.TestCase):
header = lines[0].split()
timestamp = " ".join(header[:10])
options = header[10:]
channel: WWVBChannel = "amplitude"
channel = "amplitude"
style = "default"
for o in options:
if o == "--channel=both":
channel = "both"
elif o == "--channel=amplitude":
channel = "amplitude"
elif o == "--channel=phase":
channel = "phase"
if o.startswith("--channel="):
channel = o[10:]
elif o.startswith("--style="):
style = o[8:]
else:
@ -310,11 +306,6 @@ class WWVBRoundtrip(unittest.TestCase):
wwvb._maybe_warn_update(datetime.date(1970, 1, 1))
wwvb._maybe_warn_update(datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
def test_deprecated_min(self) -> None:
"""Ensure that the 'maybe_warn_update' function is covered"""
with self.assertWarnsRegex(DeprecationWarning, "min property"):
self.assertEqual(wwvb.WWVBMinute(2021, 1, 1, 1).min, wwvb.WWVBMinute(2021, 1, 1, 1).minute)
def test_undefined(self) -> None:
"""Ensure that the check for unset elements in am works"""
with self.assertWarnsRegex(Warning, "is unset"):
@ -434,3 +425,7 @@ 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()

View file

@ -1,14 +1,7 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler
#
# SPDX-License-Identifier: CC0-1.0
#
# "For six minutes each half hour, from 1016 and 4046 minutes past each hour,
# one-minute frames are replaced by a special extended time frame. Rather than
# transmitting 35 bits of information in one minute, this transmits 7 bits
# (time of day and DST status only) over 6 minutes, giving 30 times as much
# energy per transmitted bit, a 14.8 dB improvement in the link budget compared
# to the standard one-minute time code." (wikipedia)
#
WWVB timecode: year=2021 days=311 hour=08 min=10 dst=1 ut1=-100 ly=0 ls=0 --channel=phase
2021-311 08:10 010000110100000111110110000001010110111111100110110101010001
2021-311 08:11 001001100111100011101110101111010010110010100111001000110001