Skip to content

Add type annotations #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 48 additions & 35 deletions adafruit_gps.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
import time
from micropython import const

try:
from typing import Optional, Tuple, List
from typing_extensions import Literal
from circuitpython_typing import ReadableBuffer
from busio import UART, I2C
except ImportError:
pass

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GPS.git"

Expand Down Expand Up @@ -75,7 +83,7 @@
# Internal helper parsing functions.
# These handle input that might be none or null and return none instead of
# throwing errors.
def _parse_degrees(nmea_data):
def _parse_degrees(nmea_data: str) -> int:
# Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value.
# Where ddd is the degrees, mm.mmmm is the minutes.
if nmea_data is None or len(nmea_data) < 3:
Expand All @@ -91,49 +99,49 @@ def _parse_degrees(nmea_data):
return degrees + minutes # return parsed string in the format dddmmmmmm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth updating the comment here, it mentions that a parsed string is returned, but it does appear to be an int same as it's annotated now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up just removing it since the comments below the function definition say it's "a pure degrees value". I think that's clear between that, the other comments, and the type annotation, but happy to change to improve clarity. Good catch!



def _parse_int(nmea_data):
def _parse_int(nmea_data: str) -> int:
if nmea_data is None or nmea_data == "":
return None
return int(nmea_data)


def _parse_float(nmea_data):
def _parse_float(nmea_data: str) -> float:
if nmea_data is None or nmea_data == "":
return None
return float(nmea_data)


def _parse_str(nmea_data):
def _parse_str(nmea_data: str) -> str:
if nmea_data is None or nmea_data == "":
return None
return str(nmea_data)


def _read_degrees(data, index, neg):
def _read_degrees(data: List[float], index: int, neg: str) -> float:
# This function loses precision with float32
x = data[index] / 1000000
if data[index + 1].lower() == neg:
x *= -1.0
return x


def _read_int_degrees(data, index, neg):
def _read_int_degrees(data: List[float], index: int, neg: str) -> float:
deg = data[index] // 1000000
minutes = data[index] % 1000000 / 10000
if data[index + 1].lower() == neg:
deg *= -1
return (deg, minutes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return looks like it would be a tuple to me instead of float

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, thanks for catching!



def _parse_talker(data_type):
def _parse_talker(data_type: bytes) -> Tuple[bytes, bytes]:
# Split the data_type into talker and sentence_type
if data_type[:1] == b"P": # Proprietary codes
return (data_type[:1], data_type[1:])

return (data_type[:2], data_type[2:])


def _parse_data(sentence_type, data):
def _parse_data(sentence_type: int, data: List[str]) -> Optional[List]:
"""Parse sentence data for the specified sentence type and
return a list of parameters in the correct format, or return None.
"""
Expand Down Expand Up @@ -217,7 +225,7 @@ class GPS:
GPS modules to read latitude, longitude, and more.
"""

def __init__(self, uart, debug=False):
def __init__(self, uart: UART, debug: bool = False) -> None:
self._uart = uart
# Initialize null starting values for GPS attributes.
self.timestamp_utc = None
Expand Down Expand Up @@ -253,7 +261,7 @@ def __init__(self, uart, debug=False):
self._magnetic_variation = None
self.debug = debug

def update(self):
def update(self) -> bool:
"""Check for updated data from the GPS module and process it
accordingly. Returns True if new data was processed, and False if
nothing new was received.
Expand Down Expand Up @@ -303,7 +311,7 @@ def update(self):

return result

def send_command(self, command, add_checksum=True):
def send_command(self, command: bytes, add_checksum: bool = True) -> None:
"""Send a command string to the GPS. If add_checksum is True (the
default) a NMEA checksum will automatically be computed and added.
Note you should NOT add the leading $ and trailing * to the command
Expand All @@ -320,48 +328,48 @@ def send_command(self, command, add_checksum=True):
self.write(b"\r\n")

@property
def has_fix(self):
def has_fix(self) -> bool:
"""True if a current fix for location information is available."""
return self.fix_quality is not None and self.fix_quality >= 1

@property
def has_3d_fix(self):
def has_3d_fix(self) -> bool:
"""Returns true if there is a 3d fix available.
use has_fix to determine if a 2d fix is available,
passing it the same data"""
return self.fix_quality_3d is not None and self.fix_quality_3d >= 2

@property
def datetime(self):
def datetime(self) -> Optional[time.struct_time]:
"""Return struct_time object to feed rtc.set_time_source() function"""
return self.timestamp_utc

@property
def nmea_sentence(self):
def nmea_sentence(self) -> Optional[str]:
"""Return raw_sentence which is the raw NMEA sentence read from the GPS"""
return self._raw_sentence

def read(self, num_bytes):
def read(self, num_bytes: Optional[int]) -> Optional[bytes]:
"""Read up to num_bytes of data from the GPS directly, without parsing.
Returns a bytearray with up to num_bytes or None if nothing was read"""
Returns a bytestring with up to num_bytes or None if nothing was read"""
return self._uart.read(num_bytes)

def write(self, bytestr):
def write(self, bytestr: ReadableBuffer) -> Optional[int]:
"""Write a bytestring data to the GPS directly, without parsing
or checksums"""
return self._uart.write(bytestr)

@property
def in_waiting(self):
def in_waiting(self) -> int:
"""Returns number of bytes available in UART read buffer"""
return self._uart.in_waiting

def readline(self):
"""Returns a newline terminated bytearray, must have timeout set for
def readline(self) -> Optional[bytes]:
"""Returns a newline terminated bytestring, must have timeout set for
the underlying UART or this will block forever!"""
return self._uart.readline()

def _read_sentence(self):
def _read_sentence(self) -> Optional[str]:
# Parse any NMEA sentence that is available.
# pylint: disable=len-as-condition
# This needs to be refactored when it can be tested.
Expand Down Expand Up @@ -394,7 +402,7 @@ def _read_sentence(self):
# At this point we don't have a valid sentence
return None

def _parse_sentence(self):
def _parse_sentence(self) -> Optional[Tuple[str, str]]:
sentence = self._read_sentence()

# sentence is a valid NMEA with a valid checksum
Expand All @@ -411,7 +419,7 @@ def _parse_sentence(self):
data_type = sentence[1:delimiter]
return (data_type, sentence[delimiter + 1 :])

def _update_timestamp_utc(self, time_utc, date=None):
def _update_timestamp_utc(self, time_utc: str, date: Optional[str] = None) -> None:
hours = int(time_utc[0:2])
mins = int(time_utc[2:4])
secs = int(time_utc[4:6])
Expand All @@ -431,7 +439,7 @@ def _update_timestamp_utc(self, time_utc, date=None):
(year, month, day, hours, mins, secs, 0, 0, -1)
)

def _parse_gll(self, data):
def _parse_gll(self, data: List[str]) -> bool:
# GLL - Geographic Position - Latitude/Longitude

if data is None or len(data) != 7:
Expand Down Expand Up @@ -459,7 +467,7 @@ def _parse_gll(self, data):

return True

def _parse_rmc(self, data):
def _parse_rmc(self, data: List[str]) -> bool:
# RMC - Recommended Minimum Navigation Information

if data is None or len(data) not in (12, 13):
Expand Down Expand Up @@ -505,7 +513,7 @@ def _parse_rmc(self, data):

return True

def _parse_gga(self, data):
def _parse_gga(self, data: List[str]) -> bool:
# GGA - Global Positioning System Fix Data

if data is None or len(data) != 14:
Expand Down Expand Up @@ -557,7 +565,7 @@ def _parse_gga(self, data):

return True

def _parse_gsa(self, talker, data):
def _parse_gsa(self, talker: bytes, data: List[str]) -> bool:
# GSA - GPS DOP and active satellites

if data is None or len(data) not in (17, 18):
Expand Down Expand Up @@ -596,7 +604,7 @@ def _parse_gsa(self, talker, data):

return True

def _parse_gsv(self, talker, data):
def _parse_gsv(self, talker: bytes, data: List[str]) -> bool:
# GSV - Satellites in view
# pylint: disable=too-many-branches

Expand Down Expand Up @@ -675,8 +683,13 @@ class GPS_GtopI2C(GPS):
"""

def __init__(
self, i2c_bus, *, address=_GPSI2C_DEFAULT_ADDRESS, debug=False, timeout=5
):
self,
i2c_bus: I2C,
*,
address: int = _GPSI2C_DEFAULT_ADDRESS,
debug: bool = False,
timeout: float = 5,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should timeout allow int or float Or maybe have it's default value changed to 5.0 instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems only to be used with time.monotonic() so float is a valid input. I'm in favor of changing to 5.0 if that helps makes that clearer.

) -> None:
from adafruit_bus_device import ( # pylint: disable=import-outside-toplevel
i2c_device,
)
Expand All @@ -688,7 +701,7 @@ def __init__(
self._internalbuffer = []
self._timeout = timeout

def read(self, num_bytes=1):
def read(self, num_bytes: int = 1) -> bytearray:
"""Read up to num_bytes of data from the GPS directly, without parsing.
Returns a bytearray with up to num_bytes or None if nothing was read"""
result = []
Expand All @@ -704,19 +717,19 @@ def read(self, num_bytes=1):
self._lastbyte = char # keep track of the last character approved
return bytearray(result)

def write(self, bytestr):
def write(self, bytestr: ReadableBuffer) -> None:
"""Write a bytestring data to the GPS directly, without parsing
or checksums"""
with self._i2c as i2c:
i2c.write(bytestr)

@property
def in_waiting(self):
def in_waiting(self) -> Literal[16]:
"""Returns number of bytes available in UART read buffer, always 16
since I2C does not have the ability to know how much data is available"""
return 16

def readline(self):
def readline(self) -> Optional[bytearray]:
"""Returns a newline terminated bytearray, must have timeout set for
the underlying UART or this will block forever!"""
timeout = time.monotonic() + self._timeout
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
Adafruit-Blinka
adafruit-circuitpython-busdevice
pyserial
adafruit-circuitpython-typing
typing-extensions~=4.0