diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7b58cf..c9e45be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,10 +6,6 @@ default_language_version: python: python3 repos: -- repo: https://github.com/psf/black - rev: 23.11.0 - hooks: - - id: black - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: @@ -22,14 +18,12 @@ repos: rev: v2.1.0 hooks: - id: reuse -- repo: https://github.com/pycqa/pylint - rev: v3.0.1 +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.6 hooks: - - id: pylint - additional_dependencies: ["setuptools>=68", beautifulsoup4, requests, adafruit-circuitpython-datetime, click, python-dateutil, leapseconddata] -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) - args: ['--profile', 'black'] + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index 67a2de2..11d151c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,8 @@ requires = [ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/wwvb/__version__.py" +[tool.ruff.lint] +select = ["E", "F"] +ignore = ["E741"] +[tool.ruff] +line-length = 120 diff --git a/src/uwwvb.py b/src/uwwvb.py index 9f9be4a..ce27f40 100644 --- a/src/uwwvb.py +++ b/src/uwwvb.py @@ -16,9 +16,7 @@ always_mark = set((0, 9, 19, 29, 39, 49, 59)) always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54)) bcd_weights = (1, 2, 4, 8, 10, 20, 40, 80, 100, 200, 400, 800) -WWVBMinute = namedtuple( - "WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"] -) +WWVBMinute = namedtuple("WWVBMinute", ["year", "days", "hour", "minute", "dst", "ut1", "ls", "ly"]) class WWVBDecoder: @@ -30,7 +28,9 @@ class WWVBDecoder: self.state = 1 def update(self, value: int) -> list[int] | None: - """Update the _state machine when a new symbol is received. If a possible complete _minute is received, return it; otherwise, return None""" + """Update the _state machine when a new symbol is received. + + If a possible complete _minute is received, return it; otherwise, return None""" result = None if self.state == 1: self.minute = [] diff --git a/src/wwvb/__init__.py b/src/wwvb/__init__.py index b4ac940..f1e57d5 100644 --- a/src/wwvb/__init__.py +++ b/src/wwvb/__init__.py @@ -49,9 +49,7 @@ def _maybe_warn_update(dt: datetime.date) -> None: # prospective available now. today = datetime.date.today() if _date(dt) < today + datetime.timedelta(days=330): - warnings.warn( - "Note: Running `updateiers` may provide better DUT1 and LS information" - ) + warnings.warn("Note: Running `updateiers` may provide better DUT1 and LS information") def get_dut1(dt: DateOrDatetime, *, warn_outdated: bool = True) -> float: @@ -110,9 +108,7 @@ def is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool: return isdst(t, tz) != isdst(t + datetime.timedelta(1), tz) -def get_dst_change_hour( - t: DateOrDatetime, tz: datetime.tzinfo = Mountain -) -> Optional[int]: +def get_dst_change_hour(t: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> Optional[int]: """Return the hour when DST changes""" lt0 = datetime.datetime(t.year, t.month, t.day, hour=0, tzinfo=tz) dst0 = lt0.dst() @@ -291,7 +287,9 @@ def extract_bit(v: int, p: int) -> bool: def hamming_parity(value: int) -> int: - """Compute the "hamming parity" of a 26-bit number, such as the minute-of-century [See Enhanced WWVB Broadcast Format 4.3]""" + """Compute the "hamming parity" of a 26-bit number, such as the minute-of-century + + For more details, see Enhanced WWVB Broadcast Format 4.3""" parity = 0 for i in range(4, -1, -1): bit = 0 @@ -324,7 +322,9 @@ _WWVBMinute = collections.namedtuple("_WWVBMinute", "year days hour min dst ut1 class WWVBMinute(_WWVBMinute): - """Uniquely identifies a minute of time in the WWVB system. To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.""" + """Uniquely identifies a minute of time in the WWVB system. + + To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.""" year: int hour: int @@ -391,7 +391,11 @@ class WWVBMinute(_WWVBMinute): def __str__(self) -> str: """Implement str()""" - return f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} ls={int(self.ls)}" + return ( + f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} " + f"min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} " + f"ls={int(self.ls)}" + ) def as_datetime_utc(self) -> datetime.datetime: """Convert to a UTC datetime""" @@ -401,9 +405,7 @@ class WWVBMinute(_WWVBMinute): as_datetime = as_datetime_utc - def as_datetime_local( - self, standard_time_offset: int = 7 * 3600, dst_observed: bool = True - ) -> datetime.datetime: + def as_datetime_local(self, standard_time_offset: int = 7 * 3600, dst_observed: bool = True) -> datetime.datetime: """Convert to a local datetime according to the DST bits""" u = self.as_datetime_utc() offset = datetime.timedelta(seconds=-standard_time_offset) @@ -471,12 +473,7 @@ class WWVBMinute(_WWVBMinute): century = (self.year // 100) * 100 # note: This relies on timedelta seconds never including leapseconds! return ( - int( - ( - self.as_datetime() - - datetime.datetime(century, 1, 1, tzinfo=datetime.timezone.utc) - ).total_seconds() - ) + int((self.as_datetime() - datetime.datetime(century, 1, 1, tzinfo=datetime.timezone.utc)).total_seconds()) // 60 ) @@ -497,9 +494,7 @@ class WWVBMinute(_WWVBMinute): t.am[36] = t.am[38] = AmplitudeModulation(ut1_sign) t.am[37] = AmplitudeModulation(not ut1_sign) t._put_am_bcd(abs(self.ut1) // 100, 40, 41, 42, 43) - t._put_am_bcd( - self.year, 45, 46, 47, 48, 50, 51, 52, 53 - ) # Implicitly discards all but lowest 2 digits of year + t._put_am_bcd(self.year, 45, 46, 47, 48, 50, 51, 52, 53) # Implicitly discards all but lowest 2 digits of year t.am[55] = AmplitudeModulation(self.ly) t.am[56] = AmplitudeModulation(self.ls) t._put_am_bcd(self.dst, 57, 58) @@ -604,16 +599,12 @@ class WWVBMinute(_WWVBMinute): else: self.fill_pm_timecode_regular(t) - def next_minute( - self, newut1: Optional[int] = None, newls: Optional[bool] = None - ) -> "WWVBMinute": + def next_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute": """Return an object representing the next minute""" d = self.as_datetime() + datetime.timedelta(minutes=1) return self.from_datetime(d, newut1, newls, self) - def previous_minute( - self, newut1: Optional[int] = None, newls: Optional[bool] = None - ) -> "WWVBMinute": + def previous_minute(self, newut1: Optional[int] = None, newls: Optional[bool] = None) -> "WWVBMinute": """Return an object representing the previous minute""" d = self.as_datetime() - datetime.timedelta(minutes=1) return self.from_datetime(d, newut1, newls, self) @@ -717,9 +708,7 @@ class WWVBMinuteIERS(WWVBMinute): """A WWVBMinute that uses a database of DUT1 information""" @classmethod - def _get_dut1_info( - cls, year: int, days: int, old_time: Optional[WWVBMinute] = None - ) -> Tuple[int, bool]: + def _get_dut1_info(cls, year: int, days: int, old_time: Optional[WWVBMinute] = None) -> Tuple[int, bool]: d = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1) return int(round(get_dut1(d) * 10)) * 100, isls(d) @@ -763,7 +752,10 @@ class WWVBTimecode: self.phase = [PhaseModulation.UNSET] * sz def _get_am_bcd(self, *poslist: int) -> Optional[int]: - """Convert the bits seq[positions[0]], ... seq[positions[len(positions-1)]] [in MSB order] from BCD to decimal""" + """Convert AM data to BCD + + The the bits ``self.am[poslist[i]]`` in MSB order are converted from + BCD to integer""" pos = reversed(poslist) val = [bool(self.am[p]) for p in pos] result = 0 @@ -779,7 +771,11 @@ class WWVBTimecode: return result def _put_am_bcd(self, v: int, *poslist: int) -> None: - """Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number""" + """Insert BCD coded data into the AM signal + + The bits at ``self.am[poslist[i]]`` in MSB order are filled with + the conversion of `v` to BCD + Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number""" pos = list(poslist)[::-1] for p, b in zip(pos, bcd_bits(v)): if b: @@ -798,9 +794,7 @@ class WWVBTimecode: def __str__(self) -> str: """implement str()""" - undefined = [ - i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET - ] + undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET] if undefined: warnings.warn(f"am{undefined} is unset") diff --git a/src/wwvb/decode.py b/src/wwvb/decode.py index fdb2aa7..0b2a38e 100644 --- a/src/wwvb/decode.py +++ b/src/wwvb/decode.py @@ -23,9 +23,7 @@ import wwvb always_zero = set((4, 10, 11, 14, 20, 21, 34, 35, 44, 54)) -def wwvbreceive() -> ( - Generator[Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModulation, None] -): # pylint: disable=too-many-branches +def wwvbreceive() -> Generator[Optional[wwvb.WWVBTimecode], wwvb.AmplitudeModulation, None]: # pylint: disable=too-many-branches """A stateful decoder of WWVB signals""" minute: List[wwvb.AmplitudeModulation] = [] state = 1 @@ -60,10 +58,7 @@ def wwvbreceive() -> ( elif len(minute) % 10 and value == wwvb.AmplitudeModulation.MARK: # print("UNEXPECTED MARK") state = 1 - elif ( - len(minute) - 1 in always_zero - and value != wwvb.AmplitudeModulation.ZERO - ): + elif len(minute) - 1 in always_zero and value != wwvb.AmplitudeModulation.ZERO: # print("UNEXPECTED NONZERO") state = 1 elif len(minute) == 60: diff --git a/src/wwvb/gen.py b/src/wwvb/gen.py index eac5b29..8d403e9 100755 --- a/src/wwvb/gen.py +++ b/src/wwvb/gen.py @@ -26,9 +26,7 @@ def parse_timespec( # pylint: disable=unused-argument return datetime.datetime(year, month, day, hour, minute) if len(value) == 4: year, yday, hour, minute = map(int, value) - return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta( - days=yday - 1 - ) + return datetime.datetime(year, 1, 1, hour, minute) + datetime.timedelta(days=yday - 1) if len(value) == 1: return dateutil.parser.parse(value[0]) if len(value) == 0: @@ -68,9 +66,7 @@ def parse_timespec( # pylint: disable=unused-argument help="Force no leap second at the end of the month (Implies --no-iers)", ) @click.option("--dut1", "-d", type=int, help="Force the DUT1 value (Implies --no-iers)") -@click.option( - "--minutes", "-m", default=10, help="Number of minutes to show (default: 10)" -) +@click.option("--minutes", "-m", default=10, help="Number of minutes to show (default: 10)") @click.option( "--style", default="default", @@ -127,9 +123,7 @@ def main( if style == "json": print_timecodes_json(w, minutes, channel, file=sys.stdout) else: - print_timecodes( - w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout - ) + print_timecodes(w, minutes, channel, style, all_timecodes=all_timecodes, file=sys.stdout) if __name__ == "__main__": # pragma no branch diff --git a/src/wwvb/iersdata.py b/src/wwvb/iersdata.py index d9e3741..0e5ae94 100644 --- a/src/wwvb/iersdata.py +++ b/src/wwvb/iersdata.py @@ -24,8 +24,6 @@ for location in [ exec(f.read(), globals(), globals()) # pylint: disable=exec-used break -start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace( - tzinfo=datetime.timezone.utc -) +start = datetime.datetime.combine(DUT1_DATA_START, datetime.time()).replace(tzinfo=datetime.timezone.utc) span = datetime.timedelta(days=len(DUT1_OFFSETS)) end = start + span diff --git a/src/wwvb/testcli.py b/src/wwvb/testcli.py index 7ee28d8..d74d301 100644 --- a/src/wwvb/testcli.py +++ b/src/wwvb/testcli.py @@ -8,48 +8,57 @@ # pylint: disable=invalid-name +import json import os import subprocess import sys import unittest +from typing import Any, Sequence -coverage_add = ( - ("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else () -) +coverage_add = ("-m", "coverage", "run", "--branch", "-p") if "COVERAGE_RUN" in os.environ else () class CLITestCase(unittest.TestCase): """Test various CLI commands within wwvbpy""" - def assertProgramOutput(self, expected: str, *args: str) -> None: - """Check the output from invoking a program matches the expected""" + def programOutput(self, *args: str) -> str: env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" - actual = subprocess.check_output( - args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env - ) + return subprocess.check_output(args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env) + + def moduleArgs(self, *args: str) -> Sequence[str]: + return tuple((sys.executable, *coverage_add, "-m", *args)) + + def moduleOutput(self, *args: str) -> str: + 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""" + actual = self.programOutput(*args) self.assertMultiLineEqual(expected, actual, f"args={args}") def assertProgramOutputStarts(self, expected: str, *args: str) -> None: """Check the output from invoking a program matches the expected""" - env = os.environ.copy() - env["PYTHONIOENCODING"] = "utf-8" - actual = subprocess.check_output( - args, stdin=subprocess.DEVNULL, encoding="utf-8", env=env - ) + actual = self.programOutput(*args) self.assertMultiLineEqual(expected, actual[: len(expected)], f"args={args}") def assertModuleOutput(self, expected: str, *args: str) -> None: """Check the output from invoking a `python -m modulename` program matches the expected""" - return self.assertProgramOutput( - expected, sys.executable, *coverage_add, "-m", *args - ) + actual = self.moduleOutput(*args) + self.assertMultiLineEqual(expected, actual, f"args={args}") + + 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: + """Check the output from invoking a `python -m modulename` program matches the expected""" + actual = self.moduleOutput(*args) + self.assertEqual(json.loads(actual), expected) def assertModuleOutputStarts(self, expected: str, *args: str) -> None: """Check the output from invoking a `python -m modulename` program matches the expected""" - return self.assertProgramOutputStarts( - expected, sys.executable, *coverage_add, "-m", *args - ) + actual = self.moduleOutput(*args) + self.assertStarts(expected, actual, *args) def assertProgramError(self, *args: str) -> None: """Check the output from invoking a program fails""" @@ -57,16 +66,12 @@ class CLITestCase(unittest.TestCase): env["PYTHONIOENCODING"] = "utf-8" with self.assertRaises(subprocess.SubprocessError): subprocess.check_output( - args, - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - encoding="utf-8", - env=env, + args, stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, encoding="utf-8", env=env ) def assertModuleError(self, *args: str) -> None: """Check the output from invoking a `python -m modulename` program fails""" - return self.assertProgramError(sys.executable, *coverage_add, "-m", *args) + self.assertProgramError(*self.moduleArgs(*args)) def test_gen(self) -> None: """test wwvb.gen""" @@ -153,10 +158,25 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0 def test_json(self) -> None: """Test the JSON output format""" - self.assertModuleOutput( - """\ -[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "amplitude": "210000000200000001120011001002000000010200010001020001000002", "phase": "111110011011010101000100100110011110001110111010111101001011"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "amplitude": "210000001200000001120011001002000000010200010001020001000002", "phase": "001010011100100011000101110000100001101000001111101100000010"}] -""", + self.assertModuleJson( + [ + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 40, + "amplitude": "210000000200000001120011001002000000010200010001020001000002", + "phase": "111110011011010101000100100110011110001110111010111101001011", + }, + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 41, + "amplitude": "210000001200000001120011001002000000010200010001020001000002", + "phase": "001010011100100011000101110000100001101000001111101100000010", + }, + ], "wwvb.gen", "-m", "2", @@ -166,10 +186,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0 "both", "2021-12-6 3:40", ) - self.assertModuleOutput( - """\ -[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "amplitude": "210000000200000001120011001002000000010200010001020001000002"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "amplitude": "210000001200000001120011001002000000010200010001020001000002"}] -""", + self.assertModuleJson( + [ + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 40, + "amplitude": "210000000200000001120011001002000000010200010001020001000002", + }, + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 41, + "amplitude": "210000001200000001120011001002000000010200010001020001000002", + }, + ], "wwvb.gen", "-m", "2", @@ -179,10 +212,23 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0 "amplitude", "2021-12-6 3:40", ) - self.assertModuleOutput( - """\ -[{"year": 2021, "days": 340, "hour": 3, "minute": 40, "phase": "111110011011010101000100100110011110001110111010111101001011"}, {"year": 2021, "days": 340, "hour": 3, "minute": 41, "phase": "001010011100100011000101110000100001101000001111101100000010"}] -""", + self.assertModuleJson( + [ + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 40, + "phase": "111110011011010101000100100110011110001110111010111101001011", + }, + { + "year": 2021, + "days": 340, + "hour": 3, + "minute": 41, + "phase": "001010011100100011000101110000100001101000001111101100000010", + }, + ], "wwvb.gen", "-m", "2", @@ -198,9 +244,11 @@ WWVB timecode: year=2020 days=001 hour=12 min=30 dst=0 ut1=-300 ly=1 ls=0 self.assertModuleOutput( """\ WWVB timecode: year=2021 days=340 hour=03 min=40 dst=0 ut1=-100 ly=0 ls=0 --style=sextant -2021-340 03:40 🬋🬩🬋🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬩🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬩🬹🬍🬎🬩🬹🬍🬎🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬎🬩🬹🬍🬎🬋🬎🬩🬹🬩🬹🬋🬍🬍🬎🬩🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬋🬎🬩🬹🬋🬩🬩🬹🬍🬎🬩🬹🬋🬹🬩🬹🬍🬎🬩🬹🬋🬎🬩🬹🬋🬩🬩🬹🬩🬹🬍🬎🬋🬹🬍🬎🬍🬎🬩🬹🬍🬎🬩🬹🬋🬩 +2021-340 03:40 \ +🬋🬩🬋🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬩🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬩🬹🬍🬎🬩🬹🬍🬎🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬎🬩🬹🬍🬎🬋🬎🬩🬹🬩🬹🬋🬍🬍🬎🬩🬹🬩🬹🬩🬹🬩🬹🬍🬎🬍🬎🬋🬎🬩🬹🬋🬩🬩🬹🬍🬎🬩🬹🬋🬹🬩🬹🬍🬎🬩🬹🬋🬎🬩🬹🬋🬩🬩🬹🬩🬹🬍🬎🬋🬹🬍🬎🬍🬎🬩🬹🬍🬎🬩🬹🬋🬩 -2021-340 03:41 🬋🬍🬋🬎🬩🬹🬍🬎🬩🬹🬍🬎🬍🬎🬩🬹🬋🬹🬋🬩🬍🬎🬍🬎🬩🬹🬍🬎🬍🬎🬍🬎🬩🬹🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬹🬩🬹🬩🬹🬋🬎🬍🬎🬍🬎🬋🬍🬩🬹🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬩🬹🬋🬎🬩🬹🬋🬍🬍🬎🬍🬎🬍🬎🬋🬎🬩🬹🬩🬹🬩🬹🬋🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬋🬎🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬋🬍 +2021-340 03:41 \ +🬋🬍🬋🬎🬩🬹🬍🬎🬩🬹🬍🬎🬍🬎🬩🬹🬋🬹🬋🬩🬍🬎🬍🬎🬩🬹🬍🬎🬍🬎🬍🬎🬩🬹🬋🬹🬋🬎🬋🬍🬍🬎🬩🬹🬋🬎🬋🬹🬩🬹🬩🬹🬋🬎🬍🬎🬍🬎🬋🬍🬩🬹🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬩🬹🬋🬎🬩🬹🬋🬍🬍🬎🬍🬎🬍🬎🬋🬎🬩🬹🬩🬹🬩🬹🬋🬹🬩🬹🬋🬍🬩🬹🬩🬹🬍🬎🬋🬎🬍🬎🬍🬎🬍🬎🬍🬎🬩🬹🬋🬍 """, "wwvb.gen", diff --git a/src/wwvb/testdaylight.py b/src/wwvb/testdaylight.py index c9f2803..1d6d43b 100755 --- a/src/wwvb/testdaylight.py +++ b/src/wwvb/testdaylight.py @@ -19,9 +19,7 @@ class TestDaylight(unittest.TestCase): """Test that the onset of DST is the same in Mountain and WWVBMinute (which uses ls bits)""" for h in [8, 9, 10]: for dm in range(-1441, 1442): - d = datetime.datetime( - 2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc - ) + datetime.timedelta(minutes=dm) + d = datetime.datetime(2021, 3, 14, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm) m = wwvb.WWVBMinute.from_datetime(d) self.assertEqual( m.as_datetime_local().replace(tzinfo=Mountain), @@ -32,9 +30,7 @@ class TestDaylight(unittest.TestCase): """Test that the end of DST is the same in Mountain and WWVBMinute (which uses ls bits)""" for h in [7, 8, 9]: for dm in range(-1441, 1442): - d = datetime.datetime( - 2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc - ) + datetime.timedelta(minutes=dm) + d = datetime.datetime(2021, 11, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm) m = wwvb.WWVBMinute.from_datetime(d) self.assertEqual( m.as_datetime_local().replace(tzinfo=Mountain), @@ -45,9 +41,7 @@ class TestDaylight(unittest.TestCase): """Test that middle of DST is the same in Mountain and WWVBMinute (which uses ls bits)""" for h in [7, 8, 9]: for dm in (-1, 0, 1): - d = datetime.datetime( - 2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc - ) + datetime.timedelta(minutes=dm) + d = datetime.datetime(2021, 7, 7, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm) m = wwvb.WWVBMinute.from_datetime(d) self.assertEqual( m.as_datetime_local().replace(tzinfo=Mountain), @@ -58,9 +52,7 @@ class TestDaylight(unittest.TestCase): """Test that middle of standard time is the same in Mountain and WWVBMinute (which uses ls bits)""" for h in [7, 8, 9]: for dm in (-1, 0, 1): - d = datetime.datetime( - 2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc - ) + datetime.timedelta(minutes=dm) + d = datetime.datetime(2021, 12, 25, h, 0, tzinfo=datetime.timezone.utc) + datetime.timedelta(minutes=dm) m = wwvb.WWVBMinute.from_datetime(d) self.assertEqual( m.as_datetime_local().replace(tzinfo=Mountain), diff --git a/src/wwvb/testls.py b/src/wwvb/testls.py index c210785..30d2f4e 100755 --- a/src/wwvb/testls.py +++ b/src/wwvb/testls.py @@ -55,9 +55,7 @@ class TestLeapSecond(unittest.TestCase): leap.append(nm) else: assert not our_is_ls - d = datetime.datetime.combine(nm, datetime.time()).replace( - tzinfo=datetime.timezone.utc - ) + d = datetime.datetime.combine(nm, datetime.time()).replace(tzinfo=datetime.timezone.utc) self.assertEqual(leap, bench) diff --git a/src/wwvb/testpm.py b/src/wwvb/testpm.py index dc8d377..31522b0 100755 --- a/src/wwvb/testpm.py +++ b/src/wwvb/testpm.py @@ -15,23 +15,9 @@ class TestPhaseModulation(unittest.TestCase): def test_pm(self) -> None: """Compare the generated signal from a reference minute in NIST docs""" - ref_am = ( - "2011000002" - "0001001112" - "0001010002" - "0110001012" - "0100000012" - "0010010112" - ) + ref_am = "2011000002" "0001001112" "0001010002" "0110001012" "0100000012" "0010010112" - ref_pm = ( - "0011101101" - "0001001000" - "0011001000" - "0110001101" - "0011010001" - "0110110110" - ) + ref_pm = "0011101101" "0001001000" "0011001000" "0110001101" "0011010001" "0110110110" ref_minute = wwvb.WWVBMinuteIERS(2012, 186, 17, 30, dst=3) ref_time = ref_minute.as_timecode() diff --git a/src/wwvb/testuwwvb.py b/src/wwvb/testuwwvb.py index f12396a..0afb2aa 100644 --- a/src/wwvb/testuwwvb.py +++ b/src/wwvb/testuwwvb.py @@ -24,7 +24,9 @@ class WWVBRoundtrip(unittest.TestCase): def assertDateTimeEqualExceptTzInfo( # pylint: disable=invalid-name self, a: EitherDatetimeOrNone, b: EitherDatetimeOrNone ) -> None: - """Test two datetime objects for equality, excluding tzinfo, and allowing adafruit_datetime and core datetime modules to compare equal""" + """Test two datetime objects for equality + + This equality test excludes tzinfo, and allows adafruit_datetime and core datetime modules to compare equal""" assert a assert b self.assertEqual( @@ -35,9 +37,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_decode(self) -> None: """Test decoding of some minutes including a leap second. Each minute must decode and match the primary decoder.""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) assert minute decoder = uwwvb.WWVBDecoder() decoder.update(uwwvb.MARK) @@ -60,17 +60,13 @@ class WWVBRoundtrip(unittest.TestCase): def test_roundtrip(self) -> None: """Test that some big range of times all decode the same as the primary decoder""" dt = datetime.datetime(2002, 1, 1, 0, 0) - delta = datetime.timedelta( - minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182 - ) + delta = datetime.timedelta(minutes=7182 if sys.implementation.name == "cpython" else 86400 - 7182) while dt.year < 2013: minute = wwvb.WWVBMinuteIERS.from_datetime(dt) assert minute decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am]) assert decoded - self.assertDateTimeEqualExceptTzInfo( - minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded) - ) + self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_utc(), uwwvb.as_datetime_utc(decoded)) dt = dt + delta def test_dst(self) -> None: @@ -93,9 +89,7 @@ class WWVBRoundtrip(unittest.TestCase): minute = wwvb.WWVBMinuteIERS.from_datetime(dt) decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am]) assert decoded - self.assertDateTimeEqualExceptTzInfo( - minute.as_datetime_local(), uwwvb.as_datetime_local(decoded) - ) + self.assertDateTimeEqualExceptTzInfo(minute.as_datetime_local(), uwwvb.as_datetime_local(decoded)) decoded = uwwvb.decode_wwvb([int(i) for i in minute.as_timecode().am]) assert decoded @@ -106,9 +100,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise(self) -> None: """Test of the state-machine decoder when faced with pseudorandom noise""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) r = random.Random(408) junk = [ r.choice( @@ -141,9 +133,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise2(self) -> None: """Test of the full minute decoder with targeted errors to get full coverage""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) timecode = minute.as_timecode() decoded = uwwvb.decode_wwvb([int(i) for i in timecode.am]) self.assertIsNotNone(decoded) @@ -178,9 +168,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise3(self) -> None: """Test impossible BCD values""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) timecode = minute.as_timecode() for poslist in [ diff --git a/src/wwvb/testwwvb.py b/src/wwvb/testwwvb.py index fa4934d..d5e3961 100755 --- a/src/wwvb/testwwvb.py +++ b/src/wwvb/testwwvb.py @@ -85,9 +85,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_decode(self) -> None: """Test that a range of minutes including a leap second are correctly decoded by the state-based decoder""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(1992, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50)) decoder = decode.wwvbreceive() next(decoder) decoder.send(wwvb.AmplitudeModulation.MARK) @@ -126,17 +124,13 @@ class WWVBRoundtrip(unittest.TestCase): def test_roundtrip(self) -> None: """Test that a wide of minutes are correctly decoded by the state-based decoder""" dt = datetime.datetime(1992, 1, 1, 0, 0) - delta = datetime.timedelta( - minutes=915 if sys.implementation.name == "cpython" else 86400 - 915 - ) + delta = datetime.timedelta(minutes=915 if sys.implementation.name == "cpython" else 86400 - 915) while dt.year < 1993: minute = wwvb.WWVBMinuteIERS.from_datetime(dt) assert minute is not None timecode = minute.as_timecode().am assert timecode - decoded_minute: Optional[ - wwvb.WWVBMinute - ] = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode()) + decoded_minute: Optional[wwvb.WWVBMinute] = wwvb.WWVBMinuteIERS.from_timecode_am(minute.as_timecode()) assert decoded_minute decoded = decoded_minute.as_timecode().am self.assertEqual( @@ -148,9 +142,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise(self) -> None: """Test against pseudorandom noise""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(1992, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50)) r = random.Random(408) junk = [ r.choice( @@ -180,9 +172,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise2(self) -> None: """Test of the full minute decoder with targeted errors to get full coverage""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) timecode = minute.as_timecode() decoded = wwvb.WWVBMinute.from_timecode_am(timecode) self.assertIsNotNone(decoded) @@ -217,9 +207,7 @@ class WWVBRoundtrip(unittest.TestCase): def test_noise3(self) -> None: """Test impossible BCD values""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(2012, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(2012, 6, 30, 23, 50)) timecode = minute.as_timecode() for poslist in [ @@ -241,16 +229,12 @@ class WWVBRoundtrip(unittest.TestCase): def test_previous_next_minute(self) -> None: """Test that previous minute and next minute are inverses""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(1992, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50)) self.assertEqual(minute, minute.next_minute().previous_minute()) def test_timecode_str(self) -> None: """Test the str() and repr() methods""" - minute = wwvb.WWVBMinuteIERS.from_datetime( - datetime.datetime(1992, 6, 30, 23, 50) - ) + minute = wwvb.WWVBMinuteIERS.from_datetime(datetime.datetime(1992, 6, 30, 23, 50)) timecode = minute.as_timecode() self.assertEqual( str(timecode), @@ -268,9 +252,7 @@ class WWVBRoundtrip(unittest.TestCase): sm1 = s - datetime.timedelta(days=1) self.assertEqual(wwvb.get_dut1(s), wwvb.get_dut1(sm1)) - e = iersdata.DUT1_DATA_START + datetime.timedelta( - days=len(iersdata.DUT1_OFFSETS) - 1 - ) + e = iersdata.DUT1_DATA_START + datetime.timedelta(days=len(iersdata.DUT1_OFFSETS) - 1) ep1 = e + datetime.timedelta(days=1) self.assertEqual(wwvb.get_dut1(e), wwvb.get_dut1(ep1)) @@ -290,17 +272,11 @@ class WWVBRoundtrip(unittest.TestCase): s = "WWVB timecode: year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1" t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1" - self.assertEqual( - wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t) - ) + self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)) t = "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ls=1" - self.assertEqual( - wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t) - ) + self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)) t = "year=1998 days=365 hour=23 min=56 dst=0" - self.assertEqual( - wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t) - ) + self.assertEqual(wwvb.WWVBMinuteIERS.fromstring(s), wwvb.WWVBMinuteIERS.fromstring(t)) def test_from_datetime(self) -> None: """Test the from_datetime() classmethod""" @@ -322,9 +298,7 @@ class WWVBRoundtrip(unittest.TestCase): wwvb.WWVBMinute(2021, 1, 1, 1, ls=False) with self.assertRaises(ValueError): - wwvb.WWVBMinute.fromstring( - "year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1" - ) + wwvb.WWVBMinute.fromstring("year=1998 days=365 hour=23 min=56 dst=0 ut1=-300 ly=0 ls=1 boo=1") def test_deprecated(self) -> None: """Ensure that the 'maybe_warn_update' function is covered""" @@ -357,25 +331,19 @@ class WWVBRoundtrip(unittest.TestCase): wwvb.get_dst_next(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")), 0b101111, ) - date, row = wwvb.get_dst_change_date_and_row( - datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba") - ) + date, row = wwvb.get_dst_change_date_and_row(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Cuba")) self.assertIsNone(date) self.assertIsNone(row) # California was weird in 1948 self.assertEqual( - wwvb.get_dst_next( - datetime.datetime(1948, 1, 1), tz=tz.ZoneInfo("America/Los_Angeles") - ), + wwvb.get_dst_next(datetime.datetime(1948, 1, 1), tz=tz.ZoneInfo("America/Los_Angeles")), 0b100011, ) # Berlin had DST changes on Monday in 1917 self.assertEqual( - wwvb.get_dst_next( - datetime.datetime(1917, 1, 1), tz=tz.ZoneInfo("Europe/Berlin") - ), + wwvb.get_dst_next(datetime.datetime(1917, 1, 1), tz=tz.ZoneInfo("Europe/Berlin")), 0b100011, ) @@ -383,9 +351,7 @@ class WWVBRoundtrip(unittest.TestCase): # Australia observes DST in the other half of the year compared to the # Northern hemisphere self.assertEqual( - wwvb.get_dst_next( - datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Australia/Melbourne") - ), + wwvb.get_dst_next(datetime.datetime(2005, 1, 1), tz=tz.ZoneInfo("Australia/Melbourne")), 0b100011, ) diff --git a/src/wwvb/updateiers.py b/src/wwvb/updateiers.py index 9d94a7c..5b73c69 100755 --- a/src/wwvb/updateiers.py +++ b/src/wwvb/updateiers.py @@ -27,10 +27,8 @@ try: import wwvb.iersdata_dist OLD_TABLE_START = wwvb.iersdata_dist.DUT1_DATA_START - OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta( - days=len(wwvb.iersdata_dist.DUT1_OFFSETS) - 1 - ) -except (ImportError, NameError) as e: + OLD_TABLE_END = OLD_TABLE_START + datetime.timedelta(days=len(wwvb.iersdata_dist.DUT1_OFFSETS) - 1) +except (ImportError, NameError): pass IERS_URL = "https://datacenter.iers.org/data/csv/finals2000A.all.csv" if os.path.exists("finals2000A.all.csv"): @@ -93,11 +91,7 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too- assert wwvb_dut1_table meta = wwvb_data.find("meta", property="article:modified_time") assert isinstance(meta, bs4.Tag) - wwvb_data_stamp = ( - datetime.datetime.fromisoformat(meta.attrs["content"]) - .replace(tzinfo=None) - .date() - ) + 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: off_start = (patch_start - table_start).days @@ -149,9 +143,7 @@ def update_iersdata( # pylint: disable=too-many-locals, too-many-branches, too- code(f"DUT1_DATA_START = {repr(table_start)}") c = sorted(chr(ord("a") + ch + 10) for ch in set(offsets)) code(f"{','.join(c)} = tuple({repr(''.join(c))})") - code( - f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}" - ) + code(f"DUT1_OFFSETS = str( # {table_start.year:04d}{table_start.month:02d}{table_start.day:02d}") line = "" j = 0 @@ -194,9 +186,7 @@ def iersdata_path(callback: Callable[[str, str], str]) -> str: default=iersdata_path(platformdirs.user_data_dir), ) @click.option("--dist", "location", flag_value=DIST_PATH) -@click.option( - "--site", "location", flag_value=iersdata_path(platformdirs.site_data_dir) -) +@click.option("--site", "location", flag_value=iersdata_path(platformdirs.site_data_dir)) def main(location: str) -> None: """Update DUT1 data""" print("will write to", location) diff --git a/src/wwvb/wwvbtk.py b/src/wwvb/wwvbtk.py index 28d2f71..3533b18 100755 --- a/src/wwvb/wwvbtk.py +++ b/src/wwvb/wwvbtk.py @@ -79,9 +79,7 @@ def main(colors: list[str], size: int, min_size: Optional[int]) -> None: yield timestamp + i, code timestamp = timestamp + 60 - def wwvbsmarttick() -> ( - Generator[Tuple[float, wwvb.AmplitudeModulation], None, None] - ): + def wwvbsmarttick() -> Generator[Tuple[float, wwvb.AmplitudeModulation], None, None]: """Yield consecutive values of the WWVB amplitude signal but deal with time progressing unexpectedly, such as when the computer is suspended or NTP steps the clock backwards