wwvb 3.0.8__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 +4 -1
- wwvb/__init__.py +210 -169
- wwvb/__version__.py +2 -2
- wwvb/decode.py +8 -9
- wwvb/gen.py +14 -13
- wwvb/iersdata.py +4 -5
- wwvb/iersdata_dist.py +3 -3
- wwvb/testcli.py +8 -3
- wwvb/testpm.py +2 -2
- wwvb/testuwwvb.py +41 -28
- wwvb/testwwvb.py +51 -39
- wwvb/updateiers.py +34 -31
- wwvb/wwvbtk.py +12 -10
- {wwvb-3.0.8.dist-info → wwvb-4.0.0.dist-info}/METADATA +1 -1
- wwvb-4.0.0.dist-info/RECORD +23 -0
- {wwvb-3.0.8.dist-info → wwvb-4.0.0.dist-info}/WHEEL +1 -1
- wwvb-3.0.8.dist-info/RECORD +0 -23
- {wwvb-3.0.8.dist-info → wwvb-4.0.0.dist-info}/entry_points.txt +0 -0
- {wwvb-3.0.8.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,15 +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(ctx: Any, param: Any, value:
|
21
|
+
def parse_timespec(ctx: Any, param: Any, value: list[str]) -> datetime.datetime: # noqa: ARG001
|
20
22
|
"""Parse a time specifier from the commandline"""
|
21
23
|
try:
|
22
24
|
if len(value) == 5:
|
23
25
|
year, month, day, hour, minute = map(int, value)
|
24
|
-
return datetime.datetime(year, month, day, hour, minute)
|
26
|
+
return datetime.datetime(year, month, day, hour, minute, tzinfo=datetime.timezone.utc)
|
25
27
|
if len(value) == 4:
|
26
28
|
year, yday, hour, minute = map(int, value)
|
27
|
-
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
|
+
)
|
28
32
|
if len(value) == 1:
|
29
33
|
return dateutil.parser.parse(value[0])
|
30
34
|
if len(value) == 0:
|
@@ -68,7 +72,7 @@ def parse_timespec(ctx: Any, param: Any, value: List[str]) -> datetime.datetime:
|
|
68
72
|
@click.option(
|
69
73
|
"--style",
|
70
74
|
default="default",
|
71
|
-
type=click.Choice(sorted(["json"
|
75
|
+
type=click.Choice(sorted(["json", *list(styles.keys())])),
|
72
76
|
help="Style of output",
|
73
77
|
)
|
74
78
|
@click.option(
|
@@ -86,6 +90,7 @@ def parse_timespec(ctx: Any, param: Any, value: List[str]) -> datetime.datetime:
|
|
86
90
|
)
|
87
91
|
@click.argument("timespec", type=str, nargs=-1, callback=parse_timespec)
|
88
92
|
def main(
|
93
|
+
*,
|
89
94
|
iers: bool,
|
90
95
|
leap_second: bool,
|
91
96
|
dut1: int,
|
@@ -99,7 +104,6 @@ def main(
|
|
99
104
|
|
100
105
|
TIMESPEC: one of "year yday hour minute" or "year month day hour minute", or else the current minute
|
101
106
|
"""
|
102
|
-
|
103
107
|
if (leap_second is not None) or (dut1 is not None):
|
104
108
|
iers = False
|
105
109
|
|
@@ -107,16 +111,13 @@ def main(
|
|
107
111
|
newls = None
|
108
112
|
|
109
113
|
if iers:
|
110
|
-
|
114
|
+
constructor: type[WWVBMinute] = WWVBMinuteIERS
|
111
115
|
else:
|
112
|
-
|
113
|
-
if dut1 is None
|
114
|
-
newut1 = -500 * (leap_second or 0)
|
115
|
-
else:
|
116
|
-
newut1 = dut1
|
116
|
+
constructor = WWVBMinute
|
117
|
+
newut1 = -500 * (leap_second or 0) if dut1 is None else dut1
|
117
118
|
newls = bool(leap_second)
|
118
119
|
|
119
|
-
w =
|
120
|
+
w = constructor.from_datetime(timespec, newls=newls, newut1=newut1)
|
120
121
|
if style == "json":
|
121
122
|
print_timecodes_json(w, minutes, channel, file=sys.stdout)
|
122
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())
|
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
@@ -5,9 +5,9 @@
|
|
5
5
|
# SPDX-License-Identifier: CC0-1.0
|
6
6
|
# isort: skip_file
|
7
7
|
import datetime
|
8
|
-
__all__ = [
|
8
|
+
__all__ = ["DUT1_DATA_START", "DUT1_OFFSETS"]
|
9
9
|
DUT1_DATA_START = datetime.date(1972, 1, 1)
|
10
|
-
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")
|
11
11
|
DUT1_OFFSETS = str( # 19720101
|
12
12
|
i*182+s*123+k*30+i*31+s*19+r*31+q*29+p*28+o*30+n*36+m*40 # 19730909
|
13
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
|
@@ -34,5 +34,5 @@ DUT1_OFFSETS = str( # 19720101
|
|
34
34
|
+i*126+h*176+g*97+f*91+e*52+o*116+n*98+m*70+l*133+k*91+j*91 # 20140507
|
35
35
|
+i*77+h*140+g*91+f*84+e*70+d*34+n*72+m*76+l*66+k*53+j*56 # 20160831
|
36
36
|
+i*105+h*77+g*45+q*25+p*63+o*91+n*154+m*105+l*190+k*118 # 20190501
|
37
|
-
+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
|
38
38
|
)
|
wwvb/testcli.py
CHANGED
@@ -1,6 +1,7 @@
|
|
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
|
#
|
@@ -25,7 +26,7 @@ class CLITestCase(unittest.TestCase):
|
|
25
26
|
return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env)
|
26
27
|
|
27
28
|
def moduleArgs(self, *args: str) -> Sequence[str]:
|
28
|
-
return
|
29
|
+
return (sys.executable, *coverage_add, "-m", *args)
|
29
30
|
|
30
31
|
def moduleOutput(self, *args: str) -> str:
|
31
32
|
return self.programOutput(sys.executable, *coverage_add, "-m", *args)
|
@@ -64,7 +65,11 @@ class CLITestCase(unittest.TestCase):
|
|
64
65
|
env["PYTHONIOENCODING"] = "utf-8"
|
65
66
|
with self.assertRaises(subprocess.SubprocessError):
|
66
67
|
subprocess.check_output(
|
67
|
-
args,
|
68
|
+
args,
|
69
|
+
stdin=subprocess.DEVNULL,
|
70
|
+
stderr=subprocess.DEVNULL,
|
71
|
+
encoding="utf-8",
|
72
|
+
env=env,
|
68
73
|
)
|
69
74
|
|
70
75
|
def assertModuleError(self, *args: str) -> None:
|
@@ -72,7 +77,7 @@ class CLITestCase(unittest.TestCase):
|
|
72
77
|
self.assertProgramError(*self.moduleArgs(*args))
|
73
78
|
|
74
79
|
def test_gen(self) -> None:
|
75
|
-
"""
|
80
|
+
"""Test wwvb.gen"""
|
76
81
|
self.assertModuleOutput(
|
77
82
|
"""\
|
78
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]
|
@@ -24,7 +26,8 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
24
26
|
def assertDateTimeEqualExceptTzInfo(self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone) -> None:
|
25
27
|
"""Test two datetime objects for equality
|
26
28
|
|
27
|
-
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
|
+
"""
|
28
31
|
assert a
|
29
32
|
assert b
|
30
33
|
self.assertEqual(
|
@@ -34,8 +37,10 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
34
37
|
|
35
38
|
def test_decode(self) -> None:
|
36
39
|
"""Test decoding of some minutes including a leap second.
|
37
|
-
|
38
|
-
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))
|
39
44
|
assert minute
|
40
45
|
decoder = uwwvb.WWVBDecoder()
|
41
46
|
decoder.update(uwwvb.MARK)
|
@@ -57,7 +62,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
57
62
|
|
58
63
|
def test_roundtrip(self) -> None:
|
59
64
|
"""Test that some big range of times all decode the same as the primary decoder"""
|
60
|
-
dt = datetime.datetime(2002, 1, 1, 0, 0)
|
65
|
+
dt = datetime.datetime(2002, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
61
66
|
delta = datetime.timedelta(minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182)
|
62
67
|
while dt.year < 2013:
|
63
68
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
@@ -70,19 +75,19 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
70
75
|
def test_dst(self) -> None:
|
71
76
|
"""Test of DST as handled by the small decoder"""
|
72
77
|
for dt in (
|
73
|
-
datetime.datetime(2021, 3, 14, 8, 59),
|
74
|
-
datetime.datetime(2021, 3, 14, 9, 00),
|
75
|
-
datetime.datetime(2021, 3, 14, 9, 1),
|
76
|
-
datetime.datetime(2021, 3, 15, 8, 59),
|
77
|
-
datetime.datetime(2021, 3, 15, 9, 00),
|
78
|
-
datetime.datetime(2021, 3, 15, 9, 1),
|
79
|
-
datetime.datetime(2021, 11, 7, 8, 59),
|
80
|
-
datetime.datetime(2021, 11, 7, 9, 00),
|
81
|
-
datetime.datetime(2021, 11, 7, 9, 1),
|
82
|
-
datetime.datetime(2021, 11, 8, 8, 59),
|
83
|
-
datetime.datetime(2021, 11, 8, 9, 00),
|
84
|
-
datetime.datetime(2021, 11, 8, 9, 1),
|
85
|
-
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),
|
86
91
|
):
|
87
92
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
88
93
|
decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am])
|
@@ -98,7 +103,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
98
103
|
|
99
104
|
def test_noise(self) -> None:
|
100
105
|
"""Test of the state-machine decoder when faced with pseudorandom noise"""
|
101
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
106
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
107
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
108
|
+
)
|
102
109
|
r = random.Random(408)
|
103
110
|
junk = [
|
104
111
|
r.choice(
|
@@ -106,12 +113,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
106
113
|
wwvb.AmplitudeModulation.MARK,
|
107
114
|
wwvb.AmplitudeModulation.ONE,
|
108
115
|
wwvb.AmplitudeModulation.ZERO,
|
109
|
-
]
|
116
|
+
],
|
110
117
|
)
|
111
118
|
for _ in range(480)
|
112
119
|
]
|
113
120
|
timecode = minute.as_timecode()
|
114
|
-
test_input = junk
|
121
|
+
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
|
115
122
|
decoder = uwwvb.WWVBDecoder()
|
116
123
|
for code in test_input[:-1]:
|
117
124
|
decoded = decoder.update(code)
|
@@ -131,7 +138,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
131
138
|
|
132
139
|
def test_noise2(self) -> None:
|
133
140
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
134
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
141
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
142
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
143
|
+
)
|
135
144
|
timecode = minute.as_timecode()
|
136
145
|
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
|
137
146
|
self.assertIsNotNone(decoded)
|
@@ -166,7 +175,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
166
175
|
|
167
176
|
def test_noise3(self) -> None:
|
168
177
|
"""Test impossible BCD values"""
|
169
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
178
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
179
|
+
datetime.datetime(2012, 6, 30, 23, 50, tzinfo=datetime.timezone.utc),
|
180
|
+
)
|
170
181
|
timecode = minute.as_timecode()
|
171
182
|
|
172
183
|
for poslist in [
|
@@ -191,15 +202,17 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
191
202
|
self.assertEqual(str(uwwvb.WWVBDecoder()), "<WWVBDecoder 1 []>")
|
192
203
|
|
193
204
|
def test_near_year_bug(self) -> None:
|
194
|
-
"""
|
195
|
-
|
196
|
-
|
197
|
-
|
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))
|
198
211
|
timecode = minute.as_timecode()
|
199
212
|
decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am])
|
200
213
|
assert decoded
|
201
214
|
self.assertDateTimeEqualExceptTzInfo(
|
202
|
-
datetime.datetime(2020, 12, 31, 17, 00), # Mountain time!
|
215
|
+
datetime.datetime(2020, 12, 31, 17, 00, tzinfo=zoneinfo.ZoneInfo("America/Denver")), # Mountain time!
|
203
216
|
uwwvb.as_datetime_local(decoded),
|
204
217
|
)
|
205
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
|
@@ -34,10 +38,9 @@ class WWVBTestCase(unittest.TestCase):
|
|
34
38
|
|
35
39
|
def test_cases(self) -> None:
|
36
40
|
"""Generate a test case for each expected output in tests/"""
|
37
|
-
for test in
|
41
|
+
for test in pathlib.Path("tests").glob("*"):
|
38
42
|
with self.subTest(test=test):
|
39
|
-
|
40
|
-
text = f.read()
|
43
|
+
text = test.read_text(encoding="utf-8")
|
41
44
|
lines = [line for line in text.split("\n") if not line.startswith("#")]
|
42
45
|
while not lines[0]:
|
43
46
|
del lines[0]
|
@@ -53,7 +56,7 @@ class WWVBTestCase(unittest.TestCase):
|
|
53
56
|
elif o.startswith("--style="):
|
54
57
|
style = o[8:]
|
55
58
|
else:
|
56
|
-
raise ValueError(f"Unknown option {
|
59
|
+
raise ValueError(f"Unknown option {o!r}")
|
57
60
|
num_minutes = len(lines) - 2
|
58
61
|
if channel == "both":
|
59
62
|
num_minutes = len(lines) // 3
|
@@ -84,14 +87,14 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
84
87
|
|
85
88
|
def test_decode(self) -> None:
|
86
89
|
"""Test that a range of minutes including a leap second are correctly decoded by the state-based decoder"""
|
87
|
-
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))
|
88
91
|
decoder = decode.wwvbreceive()
|
89
92
|
next(decoder)
|
90
93
|
decoder.send(wwvb.AmplitudeModulation.MARK)
|
91
94
|
any_leap_second = False
|
92
95
|
for _ in range(20):
|
93
96
|
timecode = minute.as_timecode()
|
94
|
-
decoded:
|
97
|
+
decoded: wwvb.WWVBTimecode | None = None
|
95
98
|
if len(timecode.am) == 61:
|
96
99
|
any_leap_second = True
|
97
100
|
for code in timecode.am:
|
@@ -108,28 +111,28 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
108
111
|
def test_cover_fill_pm_timecode_extended(self) -> None:
|
109
112
|
"""Get full coverage of the function pm_timecode_extended"""
|
110
113
|
for dt in (
|
111
|
-
datetime.datetime(1992, 1, 1),
|
112
|
-
datetime.datetime(1992, 4, 5),
|
113
|
-
datetime.datetime(1992, 6, 1),
|
114
|
-
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),
|
115
118
|
):
|
116
119
|
for hour in (0, 4, 11):
|
117
|
-
|
118
|
-
minute = wwvb.WWVBMinuteIERS.from_datetime(
|
120
|
+
dt1 = dt.replace(hour=hour, minute=10)
|
121
|
+
minute = wwvb.WWVBMinuteIERS.from_datetime(dt1)
|
119
122
|
assert minute is not None
|
120
123
|
timecode = minute.as_timecode().am
|
121
124
|
assert timecode
|
122
125
|
|
123
126
|
def test_roundtrip(self) -> None:
|
124
127
|
"""Test that a wide of minutes are correctly decoded by the state-based decoder"""
|
125
|
-
dt = datetime.datetime(1992, 1, 1, 0, 0)
|
128
|
+
dt = datetime.datetime(1992, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
|
126
129
|
delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915)
|
127
130
|
while dt.year < 1993:
|
128
131
|
minute = wwvb.WWVBMinuteIERS.from_datetime(dt)
|
129
132
|
assert minute is not None
|
130
133
|
timecode = minute.as_timecode().am
|
131
134
|
assert timecode
|
132
|
-
decoded_minute:
|
135
|
+
decoded_minute: wwvb.WWVBMinute | None = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode())
|
133
136
|
assert decoded_minute
|
134
137
|
decoded = decoded_minute.as_timecode().am
|
135
138
|
self.assertEqual(
|
@@ -141,7 +144,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
141
144
|
|
142
145
|
def test_noise(self) -> None:
|
143
146
|
"""Test against pseudorandom noise"""
|
144
|
-
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))
|
145
148
|
r = random.Random(408)
|
146
149
|
junk = [
|
147
150
|
r.choice(
|
@@ -149,12 +152,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
149
152
|
wwvb.AmplitudeModulation.MARK,
|
150
153
|
wwvb.AmplitudeModulation.ONE,
|
151
154
|
wwvb.AmplitudeModulation.ZERO,
|
152
|
-
]
|
155
|
+
],
|
153
156
|
)
|
154
157
|
for _ in range(480)
|
155
158
|
]
|
156
159
|
timecode = minute.as_timecode()
|
157
|
-
test_input = junk
|
160
|
+
test_input = [*junk, wwvb.AmplitudeModulation.MARK, *timecode.am]
|
158
161
|
decoder = decode.wwvbreceive()
|
159
162
|
next(decoder)
|
160
163
|
for code in test_input[:-1]:
|
@@ -171,7 +174,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
171
174
|
|
172
175
|
def test_noise2(self) -> None:
|
173
176
|
"""Test of the full minute decoder with targeted errors to get full coverage"""
|
174
|
-
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))
|
175
178
|
timecode = minute.as_timecode()
|
176
179
|
decoded = wwvb.WWVBMinute.from_timecode_am(timecode)
|
177
180
|
self.assertIsNotNone(decoded)
|
@@ -206,7 +209,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
206
209
|
|
207
210
|
def test_noise3(self) -> None:
|
208
211
|
"""Test impossible BCD values"""
|
209
|
-
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))
|
210
213
|
timecode = minute.as_timecode()
|
211
214
|
|
212
215
|
for poslist in [
|
@@ -228,12 +231,12 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
228
231
|
|
229
232
|
def test_previous_next_minute(self) -> None:
|
230
233
|
"""Test that previous minute and next minute are inverses"""
|
231
|
-
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))
|
232
235
|
self.assertEqual(minute, minute.next_minute().previous_minute())
|
233
236
|
|
234
237
|
def test_timecode_str(self) -> None:
|
235
238
|
"""Test the str() and repr() methods"""
|
236
|
-
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))
|
237
240
|
timecode = minute.as_timecode()
|
238
241
|
self.assertEqual(
|
239
242
|
str(timecode),
|
@@ -256,6 +259,9 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
256
259
|
|
257
260
|
self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1))
|
258
261
|
|
262
|
+
ep2 = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=340)
|
263
|
+
wwvb.get_dut1(ep2)
|
264
|
+
|
259
265
|
def test_epoch(self) -> None:
|
260
266
|
"""Test the 1970-to-2069 epoch"""
|
261
267
|
m = wwvb.WWVBMinute(69, 1, 1, 0, 0)
|
@@ -268,7 +274,6 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
268
274
|
|
269
275
|
def test_fromstring(self) -> None:
|
270
276
|
"""Test the fromstring() classmethod"""
|
271
|
-
|
272
277
|
s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
273
278
|
t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1"
|
274
279
|
self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t))
|
@@ -279,7 +284,7 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
279
284
|
|
280
285
|
def test_from_datetime(self) -> None:
|
281
286
|
"""Test the from_datetime() classmethod"""
|
282
|
-
d = datetime.datetime(1998, 12, 31, 23, 56, 0)
|
287
|
+
d = datetime.datetime(1998, 12, 31, 23, 56, 0, tzinfo=datetime.timezone.utc)
|
283
288
|
self.assertEqual(
|
284
289
|
wwvb.WWVBMinuteIERS.from_datetime(d),
|
285
290
|
wwvb.WWVBMinuteIERS.from_datetime(d, newls=True, newut1=-300),
|
@@ -299,16 +304,11 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
299
304
|
with self.assertRaises(ValueError):
|
300
305
|
wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1")
|
301
306
|
|
302
|
-
def test_deprecated(self) -> None:
|
303
|
-
"""Ensure that the 'maybe_warn_update' function is covered"""
|
304
|
-
with self.assertWarnsRegex(DeprecationWarning, "use ly"):
|
305
|
-
wwvb.WWVBMinute(2020, 1, 1, 1).is_ly()
|
306
|
-
|
307
307
|
def test_update(self) -> None:
|
308
308
|
"""Ensure that the 'maybe_warn_update' function is covered"""
|
309
309
|
with self.assertWarnsRegex(Warning, "updateiers"):
|
310
310
|
wwvb._maybe_warn_update(datetime.date(1970, 1, 1))
|
311
|
-
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))
|
312
312
|
|
313
313
|
def test_undefined(self) -> None:
|
314
314
|
"""Ensure that the check for unset elements in am works"""
|
@@ -317,32 +317,41 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
317
317
|
|
318
318
|
def test_tz(self) -> None:
|
319
319
|
"""Get a little more coverage in the dst change functions"""
|
320
|
-
date, row = wwvb.
|
320
|
+
date, row = wwvb._get_dst_change_date_and_row(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc))
|
321
321
|
self.assertIsNone(date)
|
322
322
|
self.assertIsNone(row)
|
323
323
|
|
324
|
-
self.assertIsNone(wwvb.
|
324
|
+
self.assertIsNone(wwvb._get_dst_change_hour(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)))
|
325
325
|
|
326
|
-
self.assertEqual(wwvb.
|
326
|
+
self.assertEqual(wwvb._get_dst_next(datetime.datetime(1960, 1, 1, tzinfo=datetime.timezone.utc)), 0b000111)
|
327
327
|
|
328
328
|
# Cuba followed year-round DST for several years
|
329
329
|
self.assertEqual(
|
330
|
-
wwvb.
|
330
|
+
wwvb._get_dst_next(datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc), tz=tz.ZoneInfo("Cuba")),
|
331
331
|
0b101111,
|
332
332
|
)
|
333
|
-
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
|
+
)
|
334
337
|
self.assertIsNone(date)
|
335
338
|
self.assertIsNone(row)
|
336
339
|
|
337
340
|
# California was weird in 1948
|
338
341
|
self.assertEqual(
|
339
|
-
wwvb.
|
342
|
+
wwvb._get_dst_next(
|
343
|
+
datetime.datetime(1948, 1, 1, tzinfo=datetime.timezone.utc),
|
344
|
+
tz=tz.ZoneInfo("America/Los_Angeles"),
|
345
|
+
),
|
340
346
|
0b100011,
|
341
347
|
)
|
342
348
|
|
343
349
|
# Berlin had DST changes on Monday in 1917
|
344
350
|
self.assertEqual(
|
345
|
-
wwvb.
|
351
|
+
wwvb._get_dst_next(
|
352
|
+
datetime.datetime(1917, 1, 1, tzinfo=datetime.timezone.utc),
|
353
|
+
tz=tz.ZoneInfo("Europe/Berlin"),
|
354
|
+
),
|
346
355
|
0b100011,
|
347
356
|
)
|
348
357
|
|
@@ -350,7 +359,10 @@ class WWVBRoundtrip(unittest.TestCase):
|
|
350
359
|
# Australia observes DST in the other half of the year compared to the
|
351
360
|
# Northern hemisphere
|
352
361
|
self.assertEqual(
|
353
|
-
wwvb.
|
362
|
+
wwvb._get_dst_next(
|
363
|
+
datetime.datetime(2005, 1, 1, tzinfo=datetime.timezone.utc),
|
364
|
+
tz=tz.ZoneInfo("Australia/Melbourne"),
|
365
|
+
),
|
354
366
|
0b100011,
|
355
367
|
)
|
356
368
|
|