wwvb 3.0.7__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uwwvb.py +5 -2
- wwvb/__init__.py +212 -179
- wwvb/__version__.py +2 -2
- wwvb/decode.py +8 -9
- wwvb/gen.py +14 -16
- wwvb/iersdata.py +4 -5
- wwvb/iersdata_dist.py +4 -6
- wwvb/testcli.py +8 -5
- wwvb/testpm.py +2 -2
- wwvb/testuwwvb.py +42 -31
- wwvb/testwwvb.py +51 -40
- wwvb/updateiers.py +35 -33
- wwvb/wwvbtk.py +13 -13
- {wwvb-3.0.7.dist-info → wwvb-4.0.0.dist-info}/METADATA +1 -1
- wwvb-4.0.0.dist-info/RECORD +23 -0
- {wwvb-3.0.7.dist-info → wwvb-4.0.0.dist-info}/WHEEL +1 -1
- wwvb-3.0.7.dist-info/RECORD +0 -23
- {wwvb-3.0.7.dist-info → wwvb-4.0.0.dist-info}/entry_points.txt +0 -0
- {wwvb-3.0.7.dist-info → wwvb-4.0.0.dist-info}/top_level.txt +0 -0
wwvb/__version__.py
CHANGED
wwvb/decode.py
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-only
|
4
4
|
"""A stateful decoder of WWVB signals"""
|
5
5
|
|
6
|
+
from __future__ import annotations
|
7
|
+
|
6
8
|
import sys
|
7
|
-
from typing import Generator
|
9
|
+
from typing import Generator
|
8
10
|
|
9
11
|
import wwvb
|
10
12
|
|
@@ -20,12 +22,12 @@ import wwvb
|
|
20
22
|
# State 4: Decoding a minute, starting in second 1
|
21
23
|
# Second
|
22
24
|
|
23
|
-
always_zero =
|
25
|
+
always_zero = {4, 10, 11, 14, 20, 21, 34, 35, 44, 54}
|
24
26
|
|
25
27
|
|
26
|
-
def wwvbreceive() -> Generator[
|
27
|
-
"""
|
28
|
-
minute:
|
28
|
+
def wwvbreceive() -> Generator[wwvb.WWVBTimecode | None, wwvb.AmplitudeModulation, None]:
|
29
|
+
"""Decode WWVB signals statefully."""
|
30
|
+
minute: list[wwvb.AmplitudeModulation] = []
|
29
31
|
state = 1
|
30
32
|
|
31
33
|
value = yield None
|
@@ -38,10 +40,7 @@ def wwvbreceive() -> Generator[Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModula
|
|
38
40
|
value = yield None
|
39
41
|
|
40
42
|
elif state == 2:
|
41
|
-
if value == wwvb.AmplitudeModulation.MARK
|
42
|
-
state = 3
|
43
|
-
else:
|
44
|
-
state = 1
|
43
|
+
state = 3 if value == wwvb.AmplitudeModulation.MARK else 1
|
45
44
|
value = yield None
|
46
45
|
|
47
46
|
elif state == 3:
|
wwvb/gen.py
CHANGED
@@ -6,9 +6,11 @@
|
|
6
6
|
#
|
7
7
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import datetime
|
10
12
|
import sys
|
11
|
-
from typing import Any
|
13
|
+
from typing import Any
|
12
14
|
|
13
15
|
import click
|
14
16
|
import dateutil.parser
|
@@ -16,17 +18,17 @@ import dateutil.parser
|
|
16
18
|
from . import WWVBMinute, WWVBMinuteIERS, print_timecodes, print_timecodes_json, styles
|
17
19
|
|
18
20
|
|
19
|
-
def parse_timespec( #
|
20
|
-
ctx: Any, param: Any, value: List[str]
|
21
|
-
) -> datetime.datetime:
|
21
|
+
def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
22
22
|
"""Parse a time specifier from the commandline"""
|
23
23
|
try:
|
24
24
|
if len(value) == 5:
|
25
25
|
year, month, day, hour, minute = map(int, value)
|
26
|
-
return datetime.datetime(year, month, day, hour, minute)
|
26
|
+
return datetime.datetime(year, month, day, hour, minute, tzinfo=datetime.timezone.utc)
|
27
27
|
if len(value) == 4:
|
28
28
|
year, yday, hour, minute = map(int, value)
|
29
|
-
return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta(
|
29
|
+
return datetime.datetime(year, 1, 1, hour, minute, tzinfo=datetime.timezone.utc) + datetime.timedelta(
|
30
|
+
days=yday - 1,
|
31
|
+
)
|
30
32
|
if len(value) == 1:
|
31
33
|
return dateutil.parser.parse(value[0])
|
32
34
|
if len(value) == 0:
|
@@ -70,7 +72,7 @@ def parse_timespec( # pylint: disable=unused-argument
|
|
70
72
|
@click.option(
|
71
73
|
"--style",
|
72
74
|
default="default",
|
73
|
-
type=click.Choice(sorted(["json"
|
75
|
+
type=click.Choice(sorted(["json", *list(styles.keys())])),
|
74
76
|
help="Style of output",
|
75
77
|
)
|
76
78
|
@click.option(
|
@@ -87,8 +89,8 @@ def parse_timespec( # pylint: disable=unused-argument
|
|
87
89
|
help="Modulation to show (default: amplitude)",
|
88
90
|
)
|
89
91
|
@click.argument("timespec", type=str, nargs=-1, callback=parse_timespec)
|
90
|
-
# pylint: disable=too-many-arguments, too-many-locals
|
91
92
|
def main(
|
93
|
+
*,
|
92
94
|
iers: bool,
|
93
95
|
leap_second: bool,
|
94
96
|
dut1: int,
|
@@ -102,7 +104,6 @@ def main(
|
|
102
104
|
|
103
105
|
TIMESPEC: one of "year yday hour minute" or "year month day hour minute", or else the current minute
|
104
106
|
"""
|
105
|
-
|
106
107
|
if (leap_second is not None) or (dut1 is not None):
|
107
108
|
iers = False
|
108
109
|
|
@@ -110,16 +111,13 @@ def main(
|
|
110
111
|
newls = None
|
111
112
|
|
112
113
|
if iers:
|
113
|
-
|
114
|
+
constructor: type[WWVBMinute] = WWVBMinuteIERS
|
114
115
|
else:
|
115
|
-
|
116
|
-
if dut1 is None
|
117
|
-
newut1 = -500 * (leap_second or 0)
|
118
|
-
else:
|
119
|
-
newut1 = dut1
|
116
|
+
constructor = WWVBMinute
|
117
|
+
newut1 = -500 * (leap_second or 0) if dut1 is None else dut1
|
120
118
|
newls = bool(leap_second)
|
121
119
|
|
122
|
-
w =
|
120
|
+
w = constructor.from_datetime(timespec, newls=newls, newut1=newut1)
|
123
121
|
if style == "json":
|
124
122
|
print_timecodes_json(w, minutes, channel, file=sys.stdout)
|
125
123
|
else:
|
wwvb/iersdata.py
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
8
|
|
9
9
|
import datetime
|
10
|
-
import
|
10
|
+
import pathlib
|
11
11
|
|
12
12
|
import platformdirs
|
13
13
|
|
@@ -18,10 +18,9 @@ for location in [
|
|
18
18
|
platformdirs.user_data_dir("wwvbpy", "unpythonic.net"),
|
19
19
|
platformdirs.site_data_dir("wwvbpy", "unpythonic.net"),
|
20
20
|
]: # pragma no cover
|
21
|
-
|
22
|
-
if
|
23
|
-
|
24
|
-
exec(f.read(), globals(), globals()) # pylint: disable=exec-used
|
21
|
+
path = pathlib.Path(location) / "wwvbpy_iersdata.py"
|
22
|
+
if path.exists():
|
23
|
+
exec(path.read_text(encoding="utf-8"), globals(), globals())
|
25
24
|
break
|
26
25
|
|
27
26
|
start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace(tzinfo=datetime.timezone.utc)
|
wwvb/iersdata_dist.py
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
# -*- python3 -*-
|
2
|
+
# fmt: off
|
2
3
|
"""File generated from public data - not subject to copyright"""
|
3
|
-
|
4
4
|
# SPDX-FileCopyrightText: Public domain
|
5
5
|
# SPDX-License-Identifier: CC0-1.0
|
6
|
-
# fmt: off
|
7
6
|
# isort: skip_file
|
8
|
-
# pylint: disable=invalid-name
|
9
7
|
import datetime
|
10
|
-
__all__ = [
|
8
|
+
__all__ = ["DUT1_DATA_START", "DUT1_OFFSETS"]
|
11
9
|
DUT1_DATA_START = datetime.date(1972, 1, 1)
|
12
|
-
d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s = tuple(
|
10
|
+
d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s = tuple("defghijklmnopqrs")
|
13
11
|
DUT1_OFFSETS = str( # 19720101
|
14
12
|
i*182+s*123+k*30+i*31+s*19+r*31+q*29+p*28+o*30+n*36+m*40 # 19730909
|
15
13
|
+l*39+k*33+j*31+i*31+h*18+r*19+q*38+p*32+o*31+n*33+m*48+l*45 # 19741010
|
@@ -36,5 +34,5 @@ DUT1_OFFSETS = str( # 19720101
|
|
36
34
|
+i*126+h*176+g*97+f*91+e*52+o*116+n*98+m*70+l*133+k*91+j*91 # 20140507
|
37
35
|
+i*77+h*140+g*91+f*84+e*70+d*34+n*72+m*76+l*66+k*53+j*56 # 20160831
|
38
36
|
+i*105+h*77+g*45+q*25+p*63+o*91+n*154+m*105+l*190+k*118 # 20190501
|
39
|
-
+j*105+i*807+j*376+k*
|
37
|
+
+j*105+i*807+j*376+k*768+l*47+k*3+l*4+k*252, # 20250705
|
40
38
|
)
|
wwvb/testcli.py
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
2
|
"""Test most wwvblib commandline programs"""
|
3
3
|
|
4
|
+
# ruff: noqa: N802 D102
|
4
5
|
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
|
5
6
|
# SPDX-FileCopyrightText: 2021 Jeff Epler
|
6
7
|
#
|
7
8
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
9
|
|
9
|
-
# pylint: disable=invalid-name
|
10
|
-
|
11
10
|
import json
|
12
11
|
import os
|
13
12
|
import subprocess
|
@@ -27,7 +26,7 @@ class CLITestCase(unittest.TestCase):
|
|
27
26
|
return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env)
|
28
27
|
|
29
28
|
def moduleArgs(self, *args: str) -> Sequence[str]:
|
30
|
-
return
|
29
|
+
return (sys.executable, *coverage_add, "-m", *args)
|
31
30
|
|
32
31
|
def moduleOutput(self, *args: str) -> str:
|
33
32
|
return self.programOutput(sys.executable, *coverage_add, "-m", *args)
|
@@ -66,7 +65,11 @@ class CLITestCase(unittest.TestCase):
|
|
66
65
|
env["PYTHONIOENCODING"] = "utf-8"
|
67
66
|
with self.assertRaises(subprocess.SubprocessError):
|
68
67
|
subprocess.check_output(
|
69
|
-
args,
|
68
|
+
args,
|
69
|
+
stdin=subprocess.DEVNULL,
|
70
|
+
stderr=subprocess.DEVNULL,
|
71
|
+
encoding="utf-8",
|
72
|
+
env=env,
|
70
73
|
)
|
71
74
|
|
72
75
|
def assertModuleError(self, *args: str) -> None:
|
@@ -74,7 +77,7 @@ class CLITestCase(unittest.TestCase):
|
|
74
77
|
self.assertProgramError(*self.moduleArgs(*args))
|
75
78
|
|
76
79
|
def test_gen(self) -> None:
|
77
|
-
"""
|
80
|
+
"""Test wwvb.gen"""
|
78
81
|
self.assertModuleOutput(
|
79
82
|
"""\
|
80
83
|
WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-200 ly=1 ls=0
|
wwvb/testpm.py
CHANGED
@@ -15,9 +15,9 @@ class TestPhaseModulation(unittest.TestCase):
|
|
15
15
|
|
16
16
|
def test_pm(self) -> None:
|
17
17
|
"""Compare the generated signal from a reference minute in NIST docs"""
|
18
|
-
ref_am = "
|
18
|
+
ref_am = "201100000200010011120001010002011000101201000000120010010112"
|
19
19
|
|
20
|
-
ref_pm = "
|
20
|
+
ref_pm = "001110110100010010000011001000011000110100110100010110110110"
|
21
21
|
|
22
22
|
ref_minute = wwvb.WWVBMinuteIERS(2012, 186, 17, 30, dst=3)
|
23
23
|
ref_time = ref_minute.as_timecode()
|
wwvb/testuwwvb.py
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
#
|
5
5
|
# SPDX-License-Identifier: GPL-3.0-only
|
6
6
|
|
7
|
+
# ruff: noqa: N802 D102
|
7
8
|
import datetime
|
8
9
|
import random
|
9
10
|
import sys
|
@@ -11,8 +12,9 @@ import unittest
|
|
11
12
|
from typing import Union
|
12
13
|
|
13
14
|
import adafruit_datetime
|
14
|
-
|
15
15
|
import uwwvb
|
16
|
+
import zoneinfo
|
17
|
+
|
16
18
|
import wwvb
|
17
19
|
|
18
20
|
EitherDatetimeOrNone = Union[None, datetime.datetime, adafruit_datetime.datetime]
|
@@ -21,12 +23,11 @@ EitherDatetimeOrNone = Union[None, datetime.datetime, adafruit_datetime.datetime
|
|
21
23
|
class WWVBRoundtrip(unittest.TestCase):
|
22
24
|
"""tests of uwwvb.py"""
|
23
25
|
|
24
|
-
def assertDateTimeEqualExceptTzInfo(
|
25
|
-
self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone
|
26
|
-
) -> None:
|
26
|
+
def assertDateTimeEqualExceptTzInfo(self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone) -> None:
|
27
27
|
"""Test two datetime objects for equality
|
28
28
|
|
29
|
-
This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal
|
29
|
+
This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal
|
30
|
+
"""
|
30
31
|
assert a
|
31
32
|
assert b
|
32
33
|
self.assertEqual(
|
@@ -36,8 +37,10 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
36
37
|
|
37
38
|
def test_decode(self) -> None:
|
38
39
|
"""Test decoding of some minutes including a leap second.
|
39
|
-
|
40
|
-
minute
|
40
|
+
|
41
|
+
Each minute must decode and match the primary decoder.
|
42
|
+
"""
|
43
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
41
44
|
assert minute
|
42
45
|
decoder = uwwvb.WWVBDecoder()
|
43
46
|
decoder.update(uwwvb.MARK)
|
@@ -59,7 +62,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
59
62
|
|
60
63
|
def test_roundtrip(self) -> None:
|
61
64
|
"""Test that some big range of times all decode the same as the primary decoder"""
|
62
|
-
dt = datetime.datetime(2002, 1, 1, 0, 0)
|
65
|
+
dt = datetime.datetime(2002, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
63
66
|
delta = datetime.timedelta(minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182)
|
64
67
|
while dt.year < 2013:
|
65
68
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
@@ -72,19 +75,19 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
72
75
|
def test_dst(self) -> None:
|
73
76
|
"""Test of DST as handled by the small decoder"""
|
74
77
|
for dt in (
|
75
|
-
datetime.datetime(2021, 3, 14, 8, 59),
|
76
|
-
datetime.datetime(2021, 3, 14, 9, 00),
|
77
|
-
datetime.datetime(2021, 3, 14, 9, 1),
|
78
|
-
datetime.datetime(2021, 3, 15, 8, 59),
|
79
|
-
datetime.datetime(2021, 3, 15, 9, 00),
|
80
|
-
datetime.datetime(2021, 3, 15, 9, 1),
|
81
|
-
datetime.datetime(2021, 11, 7, 8, 59),
|
82
|
-
datetime.datetime(2021, 11, 7, 9, 00),
|
83
|
-
datetime.datetime(2021, 11, 7, 9, 1),
|
84
|
-
datetime.datetime(2021, 11, 8, 8, 59),
|
85
|
-
datetime.datetime(2021, 11, 8, 9, 00),
|
86
|
-
datetime.datetime(2021, 11, 8, 9, 1),
|
87
|
-
datetime.datetime(2021, 7, 7, 9, 1),
|
78
|
+
datetime.datetime(2021, 3, 14, 8, 59, tzinfo=datetime.timezone.utc),
|
79
|
+
datetime.datetime(2021, 3, 14, 9, 00, tzinfo=datetime.timezone.utc),
|
80
|
+
datetime.datetime(2021, 3, 14, 9, 1, tzinfo=datetime.timezone.utc),
|
81
|
+
datetime.datetime(2021, 3, 15, 8, 59, tzinfo=datetime.timezone.utc),
|
82
|
+
datetime.datetime(2021, 3, 15, 9, 00, tzinfo=datetime.timezone.utc),
|
83
|
+
datetime.datetime(2021, 3, 15, 9, 1, tzinfo=datetime.timezone.utc),
|
84
|
+
datetime.datetime(2021, 11, 7, 8, 59, tzinfo=datetime.timezone.utc),
|
85
|
+
datetime.datetime(2021, 11, 7, 9, 00, tzinfo=datetime.timezone.utc),
|
86
|
+
datetime.datetime(2021, 11, 7, 9, 1, tzinfo=datetime.timezone.utc),
|
87
|
+
datetime.datetime(2021, 11, 8, 8, 59, tzinfo=datetime.timezone.utc),
|
88
|
+
datetime.datetime(2021, 11, 8, 9, 00, tzinfo=datetime.timezone.utc),
|
89
|
+
datetime.datetime(2021, 11, 8, 9, 1, tzinfo=datetime.timezone.utc),
|
90
|
+
datetime.datetime(2021, 7, 7, 9, 1, tzinfo=datetime.timezone.utc),
|
88
91
|
):
|
89
92
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
90
93
|
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
|
@@ -100,7 +103,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
100
103
|
|
101
104
|
def test_noise(self) -> None:
|
102
105
|
"""Test of the state-machine decoder when faced with pseudorandom noise"""
|
103
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
106
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
107
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
108
|
+
)
|
104
109
|
r = random.Random(408)
|
105
110
|
junk = [
|
106
111
|
r.choice(
|
@@ -108,12 +113,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
108
113
|
wwvb.AmplitudeModulation.MARK,
|
109
114
|
wwvb.AmplitudeModulation.ONE,
|
110
115
|
wwvb.AmplitudeModulation.ZERO,
|
111
|
-
]
|
116
|
+
],
|
112
117
|
)
|
113
118
|
for _ in range(480)
|
114
119
|
]
|
115
120
|
timecode = minute.as_timecode()
|
116
|
-
test_input = junk
|
121
|
+
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
|
117
122
|
decoder = uwwvb.WWVBDecoder()
|
118
123
|
for code in test_input[:-1]:
|
119
124
|
decoded = decoder.update(code)
|
@@ -133,7 +138,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
133
138
|
|
134
139
|
def test_noise2(self) -> None:
|
135
140
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
136
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
141
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
142
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
143
|
+
)
|
137
144
|
timecode = minute.as_timecode()
|
138
145
|
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
|
139
146
|
self.assertIsNotNone(decoded)
|
@@ -168,7 +175,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
168
175
|
|
169
176
|
def test_noise3(self) -> None:
|
170
177
|
"""Test impossible BCD values"""
|
171
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
178
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
179
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
180
|
+
)
|
172
181
|
timecode = minute.as_timecode()
|
173
182
|
|
174
183
|
for poslist in [
|
@@ -193,15 +202,17 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
193
202
|
self.assertEqual(str(uwwvb.WWVBDecoder()), "<WWVBDecoder 1 []>")
|
194
203
|
|
195
204
|
def test_near_year_bug(self) -> None:
|
196
|
-
"""
|
197
|
-
|
198
|
-
|
199
|
-
|
205
|
+
"""Test for a bug seen in another WWVB implementaiton
|
206
|
+
|
207
|
+
.. in which the hours after UTC midnight on 12-31 of a leap year would
|
208
|
+
be shown incorrectly. Check that we don't have that bug.
|
209
|
+
"""
|
210
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2021, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
|
200
211
|
timecode = minute.as_timecode()
|
201
212
|
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
|
202
213
|
assert decoded
|
203
214
|
self.assertDateTimeEqualExceptTzInfo(
|
204
|
-
datetime.datetime(2020, 12, 31, 17, 00), # Mountain time!
|
215
|
+
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
|
205
216
|
uwwvb.as_datetime_local(decoded),
|
206
217
|
)
|
207
218
|
|
wwvb/testwwvb.py
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
#!/usr/bin/python3
|
2
|
+
# ruff: noqa: E501
|
3
|
+
|
2
4
|
"""Test most wwvblib functionality"""
|
3
5
|
|
4
6
|
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
|
@@ -6,16 +8,18 @@
|
|
6
8
|
#
|
7
9
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
10
|
|
11
|
+
from __future__ import annotations
|
12
|
+
|
9
13
|
import copy
|
10
14
|
import datetime
|
11
|
-
import glob
|
12
15
|
import io
|
16
|
+
import pathlib
|
13
17
|
import random
|
14
18
|
import sys
|
15
19
|
import unittest
|
16
|
-
from typing import Optional
|
17
20
|
|
18
21
|
import uwwvb
|
22
|
+
|
19
23
|
import wwvb
|
20
24
|
|
21
25
|
from . import decode, iersdata, tz
|
@@ -27,7 +31,6 @@ class WWVBMinute2k(wwvb.WWVBMinute):
|
|
27
31
|
epoch = 2000
|
28
32
|
|
29
33
|
|
30
|
-
# pylint: disable=too-many-locals
|
31
34
|
class WWVBTestCase(unittest.TestCase):
|
32
35
|
"""Test each expected output in tests/. Some outputs are from another program, some are from us"""
|
33
36
|
|
@@ -35,10 +38,9 @@ class WWVBTestCase(unittest.TestCase):
|
|
35
38
|
|
36
39
|
def test_cases(self) -> None:
|
37
40
|
"""Generate a test case for each expected output in tests/"""
|
38
|
-
for test in
|
41
|
+
for test in pathlib.Path("tests").glob("*"):
|
39
42
|
with self.subTest(test=test):
|
40
|
-
|
41
|
-
text = f.read()
|
43
|
+
text = test.read_text(encoding="utf-8")
|
42
44
|
lines = [line for line in text.split("\n") if not line.startswith("#")]
|
43
45
|
while not lines[0]:
|
44
46
|
del lines[0]
|
@@ -54,7 +56,7 @@ class WWVBTestCase(unittest.TestCase):
|
|
54
56
|
elif o.startswith("--style="):
|
55
57
|
style = o[8:]
|
56
58
|
else:
|
57
|
-
raise ValueError(f"Unknown option {
|
59
|
+
raise ValueError(f"Unknown option {o!r}")
|
58
60
|
num_minutes = len(lines) - 2
|
59
61
|
if channel == "both":
|
60
62
|
num_minutes = len(lines) // 3
|
@@ -85,14 +87,14 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
85
87
|
|
86
88
|
def test_decode(self) -> None:
|
87
89
|
"""Test that a range of minutes including a leap second are correctly decoded by the state-based decoder"""
|
88
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
90
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
89
91
|
decoder = decode.wwvbreceive()
|
90
92
|
next(decoder)
|
91
93
|
decoder.send(wwvb.AmplitudeModulation.MARK)
|
92
94
|
any_leap_second = False
|
93
95
|
for _ in range(20):
|
94
96
|
timecode = minute.as_timecode()
|
95
|
-
decoded:
|
97
|
+
decoded: wwvb.WWVBTimecode | None = None
|
96
98
|
if len(timecode.am) == 61:
|
97
99
|
any_leap_second = True
|
98
100
|
for code in timecode.am:
|
@@ -109,28 +111,28 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
109
111
|
def test_cover_fill_pm_timecode_extended(self) -> None:
|
110
112
|
"""Get full coverage of the function pm_timecode_extended"""
|
111
113
|
for dt in (
|
112
|
-
datetime.datetime(1992, 1, 1),
|
113
|
-
datetime.datetime(1992, 4, 5),
|
114
|
-
datetime.datetime(1992, 6, 1),
|
115
|
-
datetime.datetime(1992, 10, 25),
|
114
|
+
datetime.datetime(1992, 1, 1, tzinfo=datetime.timezone.utc),
|
115
|
+
datetime.datetime(1992, 4, 5, tzinfo=datetime.timezone.utc),
|
116
|
+
datetime.datetime(1992, 6, 1, tzinfo=datetime.timezone.utc),
|
117
|
+
datetime.datetime(1992, 10, 25, tzinfo=datetime.timezone.utc),
|
116
118
|
):
|
117
119
|
for hour in (0, 4, 11):
|
118
|
-
|
119
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
120
|
+
dt1 = dt.replace(hour=hour, minute=10)
|
121
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(dt1)
|
120
122
|
assert minute is not None
|
121
123
|
timecode = minute.as_timecode().am
|
122
124
|
assert timecode
|
123
125
|
|
124
126
|
def test_roundtrip(self) -> None:
|
125
127
|
"""Test that a wide of minutes are correctly decoded by the state-based decoder"""
|
126
|
-
dt = datetime.datetime(1992, 1, 1, 0, 0)
|
128
|
+
dt = datetime.datetime(1992, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
127
129
|
delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915)
|
128
130
|
while dt.year < 1993:
|
129
131
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
130
132
|
assert minute is not None
|
131
133
|
timecode = minute.as_timecode().am
|
132
134
|
assert timecode
|
133
|
-
decoded_minute:
|
135
|
+
decoded_minute: wwvb.WWVBMinute | None = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
|
134
136
|
assert decoded_minute
|
135
137
|
decoded = decoded_minute.as_timecode().am
|
136
138
|
self.assertEqual(
|
@@ -142,7 +144,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
142
144
|
|
143
145
|
def test_noise(self) -> None:
|
144
146
|
"""Test against pseudorandom noise"""
|
145
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
147
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
146
148
|
r = random.Random(408)
|
147
149
|
junk = [
|
148
150
|
r.choice(
|
@@ -150,12 +152,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
150
152
|
wwvb.AmplitudeModulation.MARK,
|
151
153
|
wwvb.AmplitudeModulation.ONE,
|
152
154
|
wwvb.AmplitudeModulation.ZERO,
|
153
|
-
]
|
155
|
+
],
|
154
156
|
)
|
155
157
|
for _ in range(480)
|
156
158
|
]
|
157
159
|
timecode = minute.as_timecode()
|
158
|
-
test_input = junk
|
160
|
+
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
|
159
161
|
decoder = decode.wwvbreceive()
|
160
162
|
next(decoder)
|
161
163
|
for code in test_input[:-1]:
|
@@ -172,7 +174,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
172
174
|
|
173
175
|
def test_noise2(self) -> None:
|
174
176
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
175
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
177
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
176
178
|
timecode = minute.as_timecode()
|
177
179
|
decoded = wwvb.WWVBMinute.from_timecode_am(timecode)
|
178
180
|
self.assertIsNotNone(decoded)
|
@@ -207,7 +209,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
207
209
|
|
208
210
|
def test_noise3(self) -> None:
|
209
211
|
"""Test impossible BCD values"""
|
210
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50))
|
212
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
211
213
|
timecode = minute.as_timecode()
|
212
214
|
|
213
215
|
for poslist in [
|
@@ -229,12 +231,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
229
231
|
|
230
232
|
def test_previous_next_minute(self) -> None:
|
231
233
|
"""Test that previous minute and next minute are inverses"""
|
232
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
234
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
233
235
|
self.assertEqual(minute, minute.next_minute().previous_minute())
|
234
236
|
|
235
237
|
def test_timecode_str(self) -> None:
|
236
238
|
"""Test the str() and repr() methods"""
|
237
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50))
|
239
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50, tzinfo=datetime.timezone.utc))
|
238
240
|
timecode = minute.as_timecode()
|
239
241
|
self.assertEqual(
|
240
242
|
str(timecode),
|
@@ -257,6 +259,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
257
259
|
|
258
260
|
self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1))
|
259
261
|
|
262
|
+
ep2 = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=340)
|
263
|
+
wwvb.get_dut1(ep2)
|
264
|
+
|
260
265
|
def test_epoch(self) -> None:
|
261
266
|
"""Test the 1970-to-2069 epoch"""
|
262
267
|
m = wwvb.WWVBMinute(69, 1, 1, 0, 0)
|
@@ -269,7 +274,6 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
269
274
|
|
270
275
|
def test_fromstring(self) -> None:
|
271
276
|
"""Test the fromstring() classmethod"""
|
272
|
-
|
273
277
|
s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
274
278
|
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
275
279
|
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
|
@@ -280,7 +284,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
280
284
|
|
281
285
|
def test_from_datetime(self) -> None:
|
282
286
|
"""Test the from_datetime() classmethod"""
|
283
|
-
d = datetime.datetime(1998, 12, 31, 23, 56, 0)
|
287
|
+
d = datetime.datetime(1998, 12, 31, 23, 56, 0, tzinfo=datetime.timezone.utc)
|
284
288
|
self.assertEqual(
|
285
289
|
wwvb.WWVBMinuteIERS.from_datetime(d),
|
286
290
|
wwvb.WWVBMinuteIERS.from_datetime(d, newls=True, newut1=-300),
|
@@ -300,16 +304,11 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
300
304
|
with self.assertRaises(ValueError):
|
301
305
|
wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1")
|
302
306
|
|
303
|
-
def test_deprecated(self) -> None:
|
304
|
-
"""Ensure that the 'maybe_warn_update' function is covered"""
|
305
|
-
with self.assertWarnsRegex(DeprecationWarning, "use ly"):
|
306
|
-
wwvb.WWVBMinute(2020, 1, 1, 1).is_ly()
|
307
|
-
|
308
307
|
def test_update(self) -> None:
|
309
308
|
"""Ensure that the 'maybe_warn_update' function is covered"""
|
310
309
|
with self.assertWarnsRegex(Warning, "updateiers"):
|
311
310
|
wwvb._maybe_warn_update(datetime.date(1970, 1, 1))
|
312
|
-
wwvb._maybe_warn_update(datetime.datetime(1970, 1, 1, 0, 0))
|
311
|
+
wwvb._maybe_warn_update(datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc))
|
313
312
|
|
314
313
|
def test_undefined(self) -> None:
|
315
314
|
"""Ensure that the check for unset elements in am works"""
|
@@ -318,32 +317,41 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
318
317
|
|
319
318
|
def test_tz(self) -> None:
|
320
319
|
"""Get a little more coverage in the dst change functions"""
|
321
|
-
date, row = wwvb.
|
320
|
+
date, row = wwvb._get_dst_change_date_and_row(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc))
|
322
321
|
self.assertIsNone(date)
|
323
322
|
self.assertIsNone(row)
|
324
323
|
|
325
|
-
self.assertIsNone(wwvb.
|
324
|
+
self.assertIsNone(wwvb._get_dst_change_hour(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)))
|
326
325
|
|
327
|
-
self.assertEqual(wwvb.
|
326
|
+
self.assertEqual(wwvb._get_dst_next(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)), 0b000111)
|
328
327
|
|
329
328
|
# Cuba followed year-round DST for several years
|
330
329
|
self.assertEqual(
|
331
|
-
wwvb.
|
330
|
+
wwvb._get_dst_next(datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc), tz=tz.ZoneInfo("Cuba")),
|
332
331
|
0b101111,
|
333
332
|
)
|
334
|
-
date, row = wwvb.
|
333
|
+
date, row = wwvb._get_dst_change_date_and_row(
|
334
|
+
datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
|
335
|
+
tz=tz.ZoneInfo("Cuba"),
|
336
|
+
)
|
335
337
|
self.assertIsNone(date)
|
336
338
|
self.assertIsNone(row)
|
337
339
|
|
338
340
|
# California was weird in 1948
|
339
341
|
self.assertEqual(
|
340
|
-
wwvb.
|
342
|
+
wwvb._get_dst_next(
|
343
|
+
datetime.datetime(1948, 1, 1, tzinfo=datetime.timezone.utc),
|
344
|
+
tz=tz.ZoneInfo("America/Los_Angeles"),
|
345
|
+
),
|
341
346
|
0b100011,
|
342
347
|
)
|
343
348
|
|
344
349
|
# Berlin had DST changes on Monday in 1917
|
345
350
|
self.assertEqual(
|
346
|
-
wwvb.
|
351
|
+
wwvb._get_dst_next(
|
352
|
+
datetime.datetime(1917, 1, 1, tzinfo=datetime.timezone.utc),
|
353
|
+
tz=tz.ZoneInfo("Europe/Berlin"),
|
354
|
+
),
|
347
355
|
0b100011,
|
348
356
|
)
|
349
357
|
|
@@ -351,7 +359,10 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
351
359
|
# Australia observes DST in the other half of the year compared to the
|
352
360
|
# Northern hemisphere
|
353
361
|
self.assertEqual(
|
354
|
-
wwvb.
|
362
|
+
wwvb._get_dst_next(
|
363
|
+
datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
|
364
|
+
tz=tz.ZoneInfo("Australia/Melbourne"),
|
365
|
+
),
|
355
366
|
0b100011,
|
356
367
|
)
|
357
368
|
|