From 6d3fe0ee87eb3c7f3cb4fc6fa6049f57b33712d7 Mon Sep 17 00:00:00 2001
From: Greg Albrecht <gba@onbeep.com>
Date: Mon, 23 Sep 2013 23:18:52 -0700
Subject: [PATCH] updating for lint, coverage and style.

---
 LICENSE                |  13 +++
 MANIFEST.in            |   1 +
 Makefile               |  19 ++--
 aprs/classes.py        |  37 +++++++-
 aprs/decimaldegrees.py |  17 ++--
 aprs/util.py           | 129 ++++++++++++++++++--------
 requirements.txt       |   1 -
 setup.cfg              |  12 ++-
 setup.py               |  53 +++++++++--
 tests/__init__.py      |   1 +
 tests/context.py       |   4 +-
 tests/test_aprs.py     |  27 +++++-
 tests/test_util.py     | 205 +++++++++++++++++++++++++++++++++++++----
 13 files changed, 423 insertions(+), 96 deletions(-)
 create mode 100644 LICENSE
 create mode 100644 MANIFEST.in

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..353e988
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2013 OnBeep, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..738616d
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst LICENSE requirements.txt
\ No newline at end of file
diff --git a/Makefile b/Makefile
index c9d5620..bf5972d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,13 @@
-# TODO: add boilerplate to Makefile.
+# Makefile for APRS Python Module.
+#
+# Source:: https://github.com/ampledata/aprs
+# Author:: Greg Albrecht W2GMD <gba@onbeep.com>
+# Copyright:: Copyright 2013 OnBeep, Inc.
+# License:: Apache License, Version 2.0
 #
 
 
-all: install_requirements develop todo
+all: install_requirements develop
 
 develop:
 	python setup.py develop
@@ -16,13 +21,10 @@ install:
 uninstall:
 	pip uninstall -y aprs
 
-todo:
-	grep \#\ TODO Makefile
-
 clean:
 	rm -rf *.egg* build dist *.py[oc] */*.py[co] cover doctest_pypi.cfg \
 	 	nosetests.xml pylint.log *.egg output.xml flake8.log tests.log \
-		test-result.xml htmlcov fab.log
+		test-result.xml htmlcov fab.log .coverage
 
 publish:
 	python setup.py register sdist upload
@@ -41,7 +43,6 @@ clonedigger:
 	clonedigger --cpd-output aprs tests
 
 lint:
-	pylint -f parseable -i y -r y aprs/*.py tests/*.py *.py | \
-		tee pylint.log
+	pylint  -i y -r n aprs/*.py tests/*.py *.py # -f colorized
 
-test: install_requirements lint clonedigger flake8 nosetests
\ No newline at end of file
+test: install_requirements lint clonedigger flake8 nosetests
diff --git a/aprs/classes.py b/aprs/classes.py
index ae2eaa1..7f7c9fa 100755
--- a/aprs/classes.py
+++ b/aprs/classes.py
@@ -1,9 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+"""APRS Class Definitions"""
+
 __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>'
+__license__ = 'Apache License, Version 2.0'
 __copyright__ = 'Copyright 2013 OnBeep, Inc.'
-__license__ = 'Apache 2.0'
 
 
 import logging
@@ -14,18 +16,40 @@ import requests
 import kiss
 
 import aprs.constants
+import aprs.util
 
 
 class APRS(object):
 
-    logger = logging.getLogger('aprs')
-    logger.addHandler(logging.StreamHandler())
+    """APRS-IS Object."""
+
+    logger = logging.getLogger(__name__)
+    logger.setLevel(aprs.constants.LOG_LEVEL)
+    console_handler = logging.StreamHandler()
+    console_handler.setLevel(aprs.constants.LOG_LEVEL)
+    formatter = logging.Formatter(aprs.constants.LOG_FORMAT)
+    console_handler.setFormatter(formatter)
+    logger.addHandler(console_handler)
+    logger.propagate = False
 
     def __init__(self, user, password='-1', input_url=None):
         self._url = input_url or aprs.constants.APRSIS_URL
         self._auth = ' '.join(['user', user, 'pass', password])
 
     def send(self, message, headers=None):
+        """
+        Sends message to APRS-IS send-only interface.
+
+        :param message: Message to send to APRS-IS.
+        :param headers: Optional headers to post.
+        :type message: str
+        :type headers: dict
+
+        :return: True on 204 success, False otherwise.
+        :rtype: bool
+        """
+        self.logger.debug('message=%s headers=%s', message, headers)
+
         headers = headers or aprs.constants.APRSIS_HTTP_HEADERS
         content = "\n".join([self._auth, message])
 
@@ -36,6 +60,13 @@ class APRS(object):
 
 class APRSKISS(kiss.KISS):
 
+    """APRS interface for KISS serial devices."""
+
     def write(self, frame):
+        """Writes APRS-encoded frame to KISS device.
+
+        :param frame: APRS frame to write to KISS device.
+        :type frame: str
+        """
         encoded_frame = aprs.util.encode_frame(frame)
         super(APRSKISS, self).write(encoded_frame)
diff --git a/aprs/decimaldegrees.py b/aprs/decimaldegrees.py
index 304ed57..eb47ff4 100644
--- a/aprs/decimaldegrees.py
+++ b/aprs/decimaldegrees.py
@@ -123,14 +123,14 @@ def dms2decimal(degrees, minutes, seconds):
 
     """
     decimal = D(0)
-    deg = D(str(degrees))
-    min = libdecimal.getcontext().divide(D(str(minutes)), D(60))
-    sec = libdecimal.getcontext().divide(D(str(seconds)), D(3600))
+    degs = D(str(degrees))
+    mins = libdecimal.getcontext().divide(D(str(minutes)), D(60))
+    secs = libdecimal.getcontext().divide(D(str(seconds)), D(3600))
 
     if degrees >= D(0):
-        decimal = deg + min + sec
+        decimal = degs + mins + secs
     else:
-        decimal = deg - min - sec
+        decimal = degs - mins - secs
 
     return libdecimal.getcontext().normalize(decimal)
 
@@ -151,11 +151,12 @@ def dm2decimal(degrees, minutes):
     return dms2decimal(degrees, minutes, 0)
 
 
-def run_doctest():
+def run_doctest():  # pragma: no cover
+    """Runs doctests for this module."""
     import doctest
-    import decimaldegrees
+    import decimaldegrees  # pylint: disable=W0406
     return doctest.testmod(decimaldegrees)
 
 
 if __name__ == '__main__':
-    run_doctest()
+    run_doctest()  # pragma: no cover
diff --git a/aprs/util.py b/aprs/util.py
index 64451bf..bd28723 100755
--- a/aprs/util.py
+++ b/aprs/util.py
@@ -12,7 +12,6 @@ import logging
 
 import aprs.constants
 import aprs.decimaldegrees
-import aprs.util
 import kiss.constants
 
 
@@ -20,26 +19,27 @@ logger = logging.getLogger(__name__)
 logger.setLevel(aprs.constants.LOG_LEVEL)
 console_handler = logging.StreamHandler()
 console_handler.setLevel(aprs.constants.LOG_LEVEL)
-formatter = logging.Formatter(aprs.constants.LOG_FORMAT)
-console_handler.setFormatter(formatter)
+console_handler.setFormatter(
+    logging.Formatter(aprs.constants.LOG_FORMAT))
 logger.addHandler(console_handler)
 logger.propagate = False
 
 
-# http://stackoverflow.com/questions/2056750/lat-long-to-minutes-and-seconds
 def dec2dm_lat(dec):
     """Converts DecDeg to APRS Coord format.
     See: http://ember2ash.com/lat.htm
 
+    Source: http://stackoverflow.com/questions/2056750
+
     Example:
         >>> test_lat = 37.7418096
         >>> aprs_lat = dec2dm_lat(test_lat)
         >>> aprs_lat
         '3744.51N'
     """
-    dm = aprs.decimaldegrees.decimal2dm(dec)
+    dec_min = aprs.decimaldegrees.decimal2dm(dec)
 
-    deg = dm[0]
+    deg = dec_min[0]
     abs_deg = abs(deg)
 
     if not deg == abs_deg:
@@ -47,7 +47,7 @@ def dec2dm_lat(dec):
     else:
         suffix = 'N'
 
-    return ''.join([str(abs_deg), "%.2f" % dm[1], suffix])
+    return ''.join([str(abs_deg), "%.2f" % dec_min[1], suffix])
 
 
 def dec2dm_lng(dec):
@@ -60,9 +60,9 @@ def dec2dm_lng(dec):
         >>> aprs_lng
         '12223.30W'
     """
-    dm = aprs.decimaldegrees.decimal2dm(dec)
+    dec_min = aprs.decimaldegrees.decimal2dm(dec)
 
-    deg = dm[0]
+    deg = dec_min[0]
     abs_deg = abs(deg)
 
     if not deg == abs_deg:
@@ -70,22 +70,13 @@ def dec2dm_lng(dec):
     else:
         suffix = 'E'
 
-    return ''.join([str(abs_deg), "%.2f" % dm[1], suffix])
+    return ''.join([str(abs_deg), "%.2f" % dec_min[1], suffix])
 
 
-# TODO: Convert doctest to unittest.
 def decode_aprs_ascii_frame(ascii_frame):
     """
     Breaks an ASCII APRS Frame down to it's constituent parts.
 
-    Test & Example
-    ~~~~
-
-        >>> frame = 'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata'
-        >>> decode_aprs_ascii_frame(frame)
-        {'source': 'W2GMD-9', 'destination': 'APOTC1', 'text': '!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata', 'path': 'APOTC1,WIDE1-1,WIDE2-1'}
-
-
     :param frame: ASCII APRS Frame.
     :type frame: str
 
@@ -96,15 +87,15 @@ def decode_aprs_ascii_frame(ascii_frame):
     decoded_frame = {}
     frame_so_far = ''
 
-    for c in ascii_frame:
-        if '>' in c and not 'source' in decoded_frame:
+    for char in ascii_frame:
+        if '>' in char and not 'source' in decoded_frame:
             decoded_frame['source'] = frame_so_far
             frame_so_far = ''
-        elif ':' in c and not 'path' in decoded_frame:
+        elif ':' in char and not 'path' in decoded_frame:
             decoded_frame['path'] = frame_so_far
             frame_so_far = ''
         else:
-            frame_so_far = ''.join([frame_so_far, c])
+            frame_so_far = ''.join([frame_so_far, char])
 
     decoded_frame['text'] = frame_so_far
     decoded_frame['destination'] = decoded_frame['path'].split(',')[0]
@@ -113,6 +104,15 @@ def decode_aprs_ascii_frame(ascii_frame):
 
 
 def format_aprs_frame(frame):
+    """
+    Formats APRS frame-as-dict into APRS frame-as-string.
+
+    :param frame: APRS frame-as-dict
+    :type frame: dict
+
+    :return: APRS frame-as-string.
+    :rtype: str
+    """
     formatted_frame = '>'.join([frame['source'], frame['destination']])
     formatted_frame = ','.join([formatted_frame, frame['path']])
     formatted_frame = ':'.join([formatted_frame, frame['text']])
@@ -120,6 +120,15 @@ def format_aprs_frame(frame):
 
 
 def create_callsign(raw_callsign):
+    """
+    Creates callsign-as-dict from callsign-as-string.
+
+    :param raw_callsign: Callsign-as-string (with or without ssid).
+    :type raw_callsign: str
+
+    :return: Callsign-as-dict.
+    :rtype: dict
+    """
     if '-' in raw_callsign:
         call_sign, ssid = raw_callsign.split('-')
     else:
@@ -165,14 +174,12 @@ def valid_callsign(callsign):
 
     logger.debug('callsign=%s ssid=%s', callsign, ssid)
 
-    if len(callsign) < 2 or len(callsign) > 6:
-        return False
-
-    if len(str(ssid)) < 1 or len(str(ssid)) > 2:
+    if (len(callsign) < 2 or len(callsign) > 6 or len(str(ssid)) < 1 or
+                len(str(ssid)) > 2):
         return False
 
-    for c in callsign:
-        if not (c.isalpha() or c.isdigit()):
+    for char in callsign:
+        if not (char.isalpha() or char.isdigit()):
             return False
 
     if not str(ssid).isdigit():
@@ -198,6 +205,14 @@ def extract_callsign(raw_frame):
 
 
 def extract_path(start, raw_frame):
+    """Extracts path from raw APRS KISS frame.
+
+    :param start:
+    :param raw_frame: Raw APRS frame from a KISS device.
+
+    :return: Full path from APRS frame.
+    :rtype: list
+    """
     full_path = []
 
     for i in range(2, start):
@@ -207,30 +222,58 @@ def extract_path(start, raw_frame):
                 full_path.append(''.join([path, '*']))
             else:
                 full_path.append(path)
+
     return full_path
 
 
 def format_path(start, raw_frame):
+    """
+    Formats path from raw APRS KISS frame.
+
+    :param start:
+    :param raw_frame: Raw APRS KISS frame.
+
+    :return: Formatted APRS path.
+    :rtype: str
+    """
     return ','.join(extract_path(start, raw_frame))
 
 
 def encode_callsign(callsign):
+    """
+    Encodes a callsign-dict within a KISS frame.
+
+    :param callsign: Callsign-dict to encode.
+    :type callsign: dict
+
+    :return: KISS-encoded callsign.
+    :rtype: str
+    """
     call_sign = callsign['callsign']
 
-    ct = (callsign['ssid'] << 1) | 0x60
+    enc_ssid = (callsign['ssid'] << 1) | 0x60
 
     if '*' in call_sign:
         call_sign = call_sign.replace('*', '')
-        ct |= 0x80
+        enc_ssid |= 0x80
 
     while len(call_sign) < 6:
         call_sign = ''.join([call_sign, ' '])
 
     encoded = ''.join([chr(ord(p) << 1) for p in call_sign])
-    return ''.join([encoded, chr(ct)])
+    return ''.join([encoded, chr(enc_ssid)])
 
 
 def encode_frame(frame):
+    """
+    Encodes an APRS frame-as-dict as a KISS frame.
+
+    :param frame: APRS frame-as-dict to encode.
+    :type frame: dict
+
+    :return: KISS-encoded APRS frame.
+    :rtype: str
+    """
     enc_frame = ''.join([
         encode_callsign(create_callsign(frame['destination'])),
         encode_callsign(create_callsign(frame['source'])),
@@ -248,6 +291,15 @@ def encode_frame(frame):
 
 
 def decode_frame(raw_frame):
+    """
+    Decodes a KISS-encoded APRS frame.
+
+    :param raw_frame: KISS-encoded frame to decode.
+    :type raw_frame: str
+
+    :return: APRS frame-as-dict.
+    :rtype: dict
+    """
     logging.debug('raw_frame=%s', raw_frame)
     frame = {}
     frame_len = len(raw_frame)
@@ -256,9 +308,9 @@ def decode_frame(raw_frame):
         for raw_slice in range(0, frame_len):
             # Is address field length correct?
             if ord(raw_frame[raw_slice]) & 0x01 and ((raw_slice + 1) % 7) == 0:
-                n = (raw_slice + 1) / 7
+                i = (raw_slice + 1) / 7
                 # Less than 2 callsigns?
-                if 2 < n < 10:
+                if 2 < i < 10:
                     if (ord(raw_frame[raw_slice + 1]) & 0x03 == 0x03 and
                             ord(raw_frame[raw_slice + 2]) == 0xf0):
                         frame['text'] = raw_frame[raw_slice + 3:]
@@ -266,17 +318,18 @@ def decode_frame(raw_frame):
                             extract_callsign(raw_frame))
                         frame['source'] = full_callsign(
                             extract_callsign(raw_frame[7:]))
-                        frame['path'] = format_path(n, raw_frame)
+                        frame['path'] = format_path(i, raw_frame)
 
     logging.debug('frame=%s', frame)
     return frame
 
 
-def run_doctest():
+def run_doctest():  # pragma: no cover
+    """Runs doctests for this module."""
     import doctest
-    import aprs.util
+    import aprs.util  # pylint: disable=W0406
     return doctest.testmod(aprs.util)
 
 
 if __name__ == '__main__':
-    run_doctest()
+    run_doctest()  # pragma: no cover
diff --git a/requirements.txt b/requirements.txt
index 60144c7..6794368 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,3 @@ clonedigger
 httpretty
 nose
 requests
-pyserial
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index fe220ee..828204d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,17 @@
+# Nosetests configuration for APRS.
+#
+# Source:: https://github.com/ampledata/aprs
+# Author:: Greg Albrecht W2GMD <gba@onbeep.com>
+# Copyright:: Copyright 2013 OnBeep, Inc.
+# License:: Apache License, Version 2.0
+#
+
+
 [nosetests]
 with-xunit = 1
 with-coverage = 1
 cover-html = 1
 with-doctest = 1
 doctest-tests = 1
-cover-inclusive = 1
+cover-tests = 0
 cover-package = aprs
-cover-tests = 1
diff --git a/setup.py b/setup.py
index dc01831..5593251 100644
--- a/setup.py
+++ b/setup.py
@@ -1,24 +1,57 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-__author__ = 'Greg Albrecht W2GMD <gba@gregalbrecht.com>'
-__copyright__ = 'Copyright 2013 Greg Albrecht'
-__license__ = 'Creative Commons Attribution 3.0 Unported License'
+"""
+Setup for the APRS Python Module.
 
+Source:: https://github.com/ampledata/aprs
+"""
 
-import setuptools
+__author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>'
+__copyright__ = 'Copyright 2013 OnBeep, Inc.'
+__license__ = 'Apache License, Version 2.0'
 
 
-setuptools.setup(
-    version='1.0.0',
+import os
+import sys
+
+import aprs
+
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup  # pylint: disable=F0401,E0611
+
+
+packages = ['aprs']
+requires = ['requests', 'kiss']
+
+
+def publish():
+    """Function for publishing package to pypi."""
+    if sys.argv[-1] == 'publish':
+        os.system('python setup.py sdist upload')
+        sys.exit()
+
+
+publish()
+
+
+setup(
     name='aprs',
+    version=aprs.__version__,
     description='Python Bindings for APRS-IS API.',
     author='Greg Albrecht',
-    author_email='gba@gregalbrecht.com',
-    license='Creative Commons Attribution 3.0 Unported License',
+    author_email='gba@onbeep.com',
+    packages=packages,
+    package_data={'': ['LICENSE']},
+    license=open('LICENSE').read(),
+    long_description=open('README.rst').read(),
     url='https://github.com/ampledata/aprs',
     setup_requires=['nose'],
     tests_require=['coverage', 'httpretty', 'nose'],
-    install_requires=['requests']
-
+    install_requires=requires,
+    package_dir={'aprs': 'aprs'},
+    zip_safe=False,
+    include_package_data=True
 )
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..d50eb01 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests for APRS Python Module."""
diff --git a/tests/context.py b/tests/context.py
index 433ac01..a9ab68e 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -1,10 +1,12 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+"""Context for tests for APRS Python Module."""
+
 
 import os
 import sys
 
 sys.path.insert(0, os.path.abspath('..'))
 
-import aprs
+import aprs  # pylint: disable=W0611
diff --git a/tests/test_aprs.py b/tests/test_aprs.py
index 4e55f9d..33ff8e0 100644
--- a/tests/test_aprs.py
+++ b/tests/test_aprs.py
@@ -1,9 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+"""Tests for Python APRS-IS Bindings."""
+
 __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>'
+__license__ = 'Apache License, Version 2.0'
 __copyright__ = 'Copyright 2013 OnBeep, Inc.'
-__license__ = 'Apache 2.0'
 
 
 import random
@@ -22,8 +24,8 @@ POSITIVE_NUMBERS = NUMBERS[1:]
 ALPHANUM = ''.join([ALPHABET, NUMBERS])
 
 
-class APRSTest(unittest.TestCase):
-    """Tests for Python APRS Bindings."""
+class APRSTest(unittest.TestCase):  # pylint: disable=R0904
+    """Tests for Python APRS-IS Bindings."""
 
     logger = logging.getLogger(__name__)
     logger.setLevel(aprs.constants.LOG_LEVEL)
@@ -35,9 +37,17 @@ class APRSTest(unittest.TestCase):
     logger.propagate = False
 
     def random(self, length=8, alphabet=ALPHANUM):
+        """
+        Generates a random string for test cases.
+
+        :param length: Length of string to generate.
+        :param alphabet: Alphabet to use to create string.
+        :type length: int
+        :type alphabet: str
+        """
         return ''.join(random.choice(alphabet) for _ in xrange(length))
 
-    def setUp(self):
+    def setUp(self):  # pylint: disable=C0103
         self.fake_server = ''.join([
             'http://localhost:',
             self.random(4, POSITIVE_NUMBERS),
@@ -60,6 +70,9 @@ class APRSTest(unittest.TestCase):
 
     @httpretty.httprettified
     def test_fake_good_auth(self):
+        """
+        Tests authenticating against APRS-IS using a valid call+pass.
+        """
         httpretty.HTTPretty.register_uri(
             httpretty.HTTPretty.POST,
             self.fake_server,
@@ -83,6 +96,9 @@ class APRSTest(unittest.TestCase):
 
     @httpretty.httprettified
     def test_fake_bad_auth(self):
+        """
+        Tests authenticating against APRS-IS using an invalid call+pass.
+        """
         httpretty.HTTPretty.register_uri(
             httpretty.HTTPretty.POST,
             self.fake_server,
@@ -106,6 +122,9 @@ class APRSTest(unittest.TestCase):
 
     @unittest.skip('Test only works with real server.')
     def test_more(self):
+        """
+        Tests APRS-IS binding against a real APRS-IS server.
+        """
         aprs_conn = aprs.APRS(
             user=self.real_callsign,
             input_url=self.real_server
diff --git a/tests/test_util.py b/tests/test_util.py
index 21f2a4a..8fba5ba 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -1,9 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+"""Tests for Python APRS util methods."""
+
 __author__ = 'Greg Albrecht W2GMD <gba@onbeep.com>'
 __copyright__ = 'Copyright 2013 OnBeep, Inc.'
-__license__ = 'Apache 2.0'
+__license__ = 'Apache License, Version 2.0'
 
 
 import unittest
@@ -25,7 +27,7 @@ INVALID_CALLSIGNS = ['xW2GMDx', 'W2GMD-16', 'W2GMD-A', 'W', 'W2GMD-1-0',
                      'W*GMD', 'W2GMD-123']
 
 
-class APRSUtilTestCase(unittest.TestCase):
+class APRSUtilTestCase(unittest.TestCase):  # pylint: disable=R0904
     """Tests for Python APRS Utils."""
 
     logger = logging.getLogger(__name__)
@@ -37,16 +39,16 @@ class APRSUtilTestCase(unittest.TestCase):
     logger.addHandler(console_handler)
     logger.propagate = False
 
-    def setUp(self):
+    def setUp(self):  # pylint: disable=C0103
         """Setup."""
         self.test_frames = open(constants.TEST_FRAMES, 'r')
         self.test_frame = self.test_frames.readlines()[0].strip()
 
-    def tearDown(self):
+    def tearDown(self):  # pylint: disable=C0103
         """Teardown."""
         self.test_frames.close()
 
-    def test_latitude(self):
+    def test_latitude_north(self):
         """Test Decimal to APRS Latitude conversion.
 
         Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf
@@ -70,14 +72,45 @@ class APRSUtilTestCase(unittest.TestCase):
         self.logger.debug('aprs_lat=%s', aprs_lat)
 
         lat_deg = int(aprs_lat.split('.')[0][:1])
-        lat_hsec = aprs_lat.split('.')[1]
+        #lat_hsec = aprs_lat.split('.')[1]
 
         self.assertTrue(len(aprs_lat) == 8)
         self.assertTrue(lat_deg >= 00)
         self.assertTrue(lat_deg <= 90)
         self.assertTrue(aprs_lat.endswith('N'))
 
-    def test_longitude(self):
+    def test_latitude_south(self):
+        """Test Decimal to APRS Latitude conversion.
+
+        Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf
+        --
+        Latitude is expressed as a fixed 8-character field, in degrees and
+        decimal minutes (to two decimal places), followed by the letter N for
+        north or S for south. Latitude degrees are in the range 00 to 90.
+        Latitude minutes are expressed as whole minutes and hundredths of a
+        minute, separated by a decimal point.
+
+        For example:
+
+            4903.50N is 49 degrees 3 minutes 30 seconds north.
+
+        In generic format examples, the latitude is shown as the 8-character
+        string ddmm.hhN (i.e. degrees, minutes and hundredths of a minute
+        north).
+        """
+        test_lat = -37.7418096
+        aprs_lat = aprs.util.dec2dm_lat(test_lat)
+        self.logger.debug('aprs_lat=%s', aprs_lat)
+
+        lat_deg = int(aprs_lat.split('.')[0][:1])
+        #lat_hsec = aprs_lat.split('.')[1]
+
+        self.assertTrue(len(aprs_lat) == 8)
+        self.assertTrue(lat_deg >= 00)
+        self.assertTrue(lat_deg <= 90)
+        self.assertTrue(aprs_lat.endswith('S'))
+
+    def test_longitude_west(self):
         """Test Decimal to APRS Longitude conversion.
 
         Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf
@@ -89,7 +122,7 @@ class APRSUtilTestCase(unittest.TestCase):
         Longitude degrees are in the range 000 to 180. Longitude minutes are
         expressed as whole minutes and hundredths of a minute, separated by a
         decimal point.
-        
+
         For example:
 
             07201.75W is 72 degrees 1 minute 45 seconds west.
@@ -103,31 +136,84 @@ class APRSUtilTestCase(unittest.TestCase):
         self.logger.debug('aprs_lng=%s', aprs_lng)
 
         lng_deg = int(aprs_lng.split('.')[0][:2])
-        lng_hsec = aprs_lng.split('.')[1]
+        #lng_hsec = aprs_lng.split('.')[1]
 
         self.assertTrue(len(aprs_lng) == 9)
         self.assertTrue(lng_deg >= 000)
         self.assertTrue(lng_deg <= 180)
         self.assertTrue(aprs_lng.endswith('W'))
 
+    def test_longitude_east(self):
+        """Test Decimal to APRS Longitude conversion.
+
+        Spec per ftp://ftp.tapr.org/aprssig/aprsspec/spec/aprs101/APRS101.pdf
+        --
+        Longitude is expressed as a fixed 9-character field, in degrees and
+        decimal minutes (to two decimal places), followed by the letter E for
+        east or W for west.
+
+        Longitude degrees are in the range 000 to 180. Longitude minutes are
+        expressed as whole minutes and hundredths of a minute, separated by a
+        decimal point.
+
+        For example:
+
+            07201.75W is 72 degrees 1 minute 45 seconds west.
+
+        In generic format examples, the longitude is shown as the 9-character
+        string dddmm.hhW (i.e. degrees, minutes and hundredths of a minute
+        west).
+        """
+        test_lng = 122.38833
+        aprs_lng = aprs.util.dec2dm_lng(test_lng)
+        self.logger.debug('aprs_lng=%s', aprs_lng)
+
+        lng_deg = int(aprs_lng.split('.')[0][:2])
+        #lng_hsec = aprs_lng.split('.')[1]
+
+        self.assertTrue(len(aprs_lng) == 9)
+        self.assertTrue(lng_deg >= 000)
+        self.assertTrue(lng_deg <= 180)
+        self.assertTrue(aprs_lng.endswith('E'))
+
     def test_valid_callsign_valid(self):
-        for c in VALID_CALLSIGNS:
-            self.assertTrue(aprs.util.valid_callsign(c), "%s is a valid call" % c)
+        """
+        Tests valid callsigns using `aprs.util.valid_callsign()`.
+        """
+        for i in VALID_CALLSIGNS:
+            self.assertTrue(
+                aprs.util.valid_callsign(i), "%s is a valid call" % i)
 
     def test_valid_callsign_invalid(self):
-        for c in INVALID_CALLSIGNS:
-            self.assertFalse(aprs.util.valid_callsign(c), "%s is an invalid call" % c)
+        """
+        Tests invalid callsigns using `aprs.util.valid_callsign()`.
+        """
+        for i in INVALID_CALLSIGNS:
+            self.assertFalse(
+                aprs.util.valid_callsign(i), "%s is an invalid call" % i)
 
     def test_extract_callsign_source(self):
+        """
+        Tests extracting the source callsign from a KISS-encoded APRS frame
+        using `aprs.util.extract_callsign()`.
+        """
         callsign = {'callsign': 'W2GMD', 'ssid': 6}
         extracted_callsign = aprs.util.extract_callsign(self.test_frame[7:])
         self.assertEqual(callsign, extracted_callsign)
 
-    def test_extract_callsign_destination(self):
+    def test_extract_callsign_dest(self):
+        """
+        Tests extracting the destination callsign from a KISS-encoded APRS
+        frame using `aprs.util.extract_callsign()`.
+        """
         extracted_callsign = aprs.util.extract_callsign(self.test_frame)
         self.assertEqual(extracted_callsign['callsign'], 'APRX24')
 
     def test_full_callsign_with_ssid(self):
+        """
+        Tests creating a full callsign string from a callsign+ssid dict using
+        `aprs.util.full_callsign()`.
+        """
         callsign = {
             'callsign': 'W2GMD',
             'ssid': 1
@@ -136,6 +222,10 @@ class APRSUtilTestCase(unittest.TestCase):
         self.assertEqual(full_callsign, 'W2GMD-1')
 
     def test_full_callsign_sans_ssid(self):
+        """
+        Tests creating a full callsign string from a callsign dict using
+        `aprs.util.full_callsign()`.
+        """
         callsign = {
             'callsign': 'W2GMD',
             'ssid': 0
@@ -144,6 +234,10 @@ class APRSUtilTestCase(unittest.TestCase):
         self.assertEqual(full_callsign, 'W2GMD')
 
     def test_format_aprs_frame(self):
+        """
+        Tests formatting an APRS frame-as-string from an APRS frame-as-dict
+        using `aprs.util.format_aprs_frame()`.
+        """
         frame = {
             'source': 'W2GMD-1',
             'destination': 'OMG',
@@ -157,22 +251,46 @@ class APRSUtilTestCase(unittest.TestCase):
         )
 
     def test_decode_aprs_ascii_frame(self):
-        ascii_frame = 'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata'
+        """
+        Tests creating an APRS frame-as-dict from an APRS frame-as-string
+        using `aprs.util.decode_aprs_ascii_frame()`.
+        """
+        ascii_frame = (
+            'W2GMD-9>APOTC1,WIDE1-1,WIDE2-1:!3745.94N/12228.05W>118/010/'
+            'A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata')
         frame = aprs.util.decode_aprs_ascii_frame(ascii_frame)
         self.assertEqual(
-            frame,
-            {'source': 'W2GMD-9', 'destination': 'APOTC1', 'text': '!3745.94N/12228.05W>118/010/A=000269 38C=Temp http://w2gmd.org/ Twitter: @ampledata', 'path': 'APOTC1,WIDE1-1,WIDE2-1'}
+            {
+                'source': 'W2GMD-9',
+                'destination': 'APOTC1',
+                'path': 'APOTC1,WIDE1-1,WIDE2-1',
+                'text': ('!3745.94N/12228.05W>118/010/A=000269 38C=Temp '
+                         'http://w2gmd.org/ Twitter: @ampledata'),
+            },
+            frame
         )
 
     def test_extract_path(self):
+        """
+        Tests extracting the APRS path from a KISS-encoded frame
+        using `aprs.util.extract_path()`.
+        """
         extracted_path = aprs.util.extract_path(3, self.test_frame)
         self.assertEqual('WIDE1-1', extracted_path[0])
 
     def test_format_path(self):
+        """
+        Tests formatting an APRS path from a KISS-encoded frame
+        using `aprs.util.format_path()`.
+        """
         extracted_path = aprs.util.format_path(3, self.test_frame)
         self.assertEqual('WIDE1-1', extracted_path)
 
     def test_encode_frame(self):
+        """
+        Tests KISS-encoding an APRS frame using
+        `aprs.util.encode_frame()`.
+        """
         frame = {
             'source': 'W2GMD-1',
             'destination': 'OMG',
@@ -180,19 +298,29 @@ class APRSUtilTestCase(unittest.TestCase):
             'text': 'test_encode_frame'
         }
         encoded_frame = aprs.util.encode_frame(frame)
-        legit = '\x9e\x9a\x8e@@@`\xaed\x8e\x9a\x88@b\xae\x92\x88\x8ab@c\x03\xf0test_encode_frame'
+        legit = ('\x9e\x9a\x8e@@@`\xaed\x8e\x9a\x88@b'
+                 '\xae\x92\x88\x8ab@c\x03\xf0test_encode_frame')
         self.assertEqual(legit, encoded_frame)
 
     def test_decode_frame_recorded(self):
+        """
+        Tests decoding a KISS-encoded APRS frame using
+        `aprs.util.decode_frame()`.
+        """
         frame = {
-            'text': '!3745.75NI12228.05W#W2GMD-6 Inner Sunset, SF iGate/Digipeater http://w2gmd.org',
             'path': 'WIDE1-1',
             'destination': 'APRX24',
-            'source': 'W2GMD-6'
+            'source': 'W2GMD-6',
+            'text': ('!3745.75NI12228.05W#W2GMD-6 Inner Sunset, '
+                     'SF iGate/Digipeater http://w2gmd.org')
         }
         self.assertEqual(frame, aprs.util.decode_frame(self.test_frame))
 
     def test_decode_frame(self):
+        """
+        Tests decoding a KISS-encoded APRS frame using
+        `aprs.util.decode_frame()`.
+        """
         frame = {
             'source': 'W2GMD-1',
             'destination': 'OMG',
@@ -203,5 +331,42 @@ class APRSUtilTestCase(unittest.TestCase):
         decoded_frame = aprs.util.decode_frame(encoded_frame)
         self.assertEqual(frame, decoded_frame)
 
+    def test_create_callsign(self):
+        """
+        Tests creating a callsign string from a callsign dict using
+        `aprs.util.create_callsign()`.
+        """
+        full_callsign = 'W2GMD-1'
+        callsign = aprs.util.create_callsign(full_callsign)
+        self.assertEqual({'callsign': 'W2GMD', 'ssid': 1}, callsign)
+
+    def test_full_callsign(self):
+        """
+        Tests converting a callsign dict to a callsing string
+        (callsign-ssid) using `aprs.util.full_callsign()`.
+        """
+        callsign = {'callsign': 'W2GMD', 'ssid': 1}
+        full_callsign = aprs.util.full_callsign(callsign)
+        self.assertEqual('W2GMD-1', full_callsign)
+
+    def test_encode_callsign_digipeated(self):
+        """
+        Tests encoding a digipeated callsign with
+        `aprs.util.encode_callsign()`.
+        """
+        callsign = {'callsign': 'W2GMD*', 'ssid': 1}
+        encoded_callsign = aprs.util.encode_callsign(callsign)
+        self.assertEqual('\xaed\x8e\x9a\x88@\xe2', encoded_callsign)
+
+    def test_encode_callsign(self):
+        """
+        Tests encoding a non-digipeated callsign with
+        `aprs.util.encode_callsign()`.
+        """
+        callsign = {'callsign': 'W2GMD', 'ssid': 1}
+        encoded_callsign = aprs.util.encode_callsign(callsign)
+        self.assertEqual('\xaed\x8e\x9a\x88@b', encoded_callsign)
+
+
 if __name__ == '__main__':
     unittest.main()
-- 
GitLab