Update tox environment to fix formatting python errors

This patch includes lots of changes to tox environment for
automatically detecting pep8 failures, which can cause python2 vs
python3 failures after install.

The following tox commands have been added
tox -efmt-check  - This checks the python syntax and formatting
tox -efmt        - Automatically fixes python syntax formatting that
                   fmt-check complains about.
tox -etype-check - check on types
tox -elint       - flake8 run

This patch also changes where the default config file is located.
The new location is ~/.config/aprsd/aprsd.yml

You can now also specify a custom config file on the command line
with the -c or --config option as well.
This commit is contained in:
Hemna 2020-12-09 14:13:35 -05:00
parent 2bebd83449
commit 53b8f21535
14 changed files with 750 additions and 391 deletions

22
.github/workflows/python.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: python
on: [push]
jobs:
tox:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [2.7, 3.6, 3.7, 3.8, 3.9]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox

View File

@ -14,6 +14,4 @@
import pbr.version
__version__ = pbr.version.VersionInfo(
'aprsd').version_string()
__version__ = pbr.version.VersionInfo("aprsd").version_string()

View File

@ -1,33 +1,27 @@
import argparse
import logging
import socketserver
import sys
import time
import socketserver
from logging.handlers import RotatingFileHandler
from aprsd import utils
# command line args
parser = argparse.ArgumentParser()
parser.add_argument("--loglevel",
default='DEBUG',
choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
help="The log level to use for aprsd.log")
parser.add_argument("--quiet",
action='store_true',
help="Don't log to stdout")
parser.add_argument(
"--loglevel",
default="DEBUG",
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
help="The log level to use for aprsd.log",
)
parser.add_argument("--quiet", action="store_true", help="Don't log to stdout")
parser.add_argument("--port",
default=9099,
type=int,
help="The port to listen on .")
parser.add_argument("--ip",
default='127.0.0.1',
help="The IP to listen on ")
parser.add_argument("--port", default=9099, type=int, help="The port to listen on .")
parser.add_argument("--ip", default="127.0.0.1", help="The IP to listen on ")
CONFIG = None
LOG = logging.getLogger('ARPSSERVER')
LOG = logging.getLogger("ARPSSERVER")
# Setup the logging faciility
@ -36,22 +30,19 @@ LOG = logging.getLogger('ARPSSERVER')
def setup_logging(args):
global LOG
levels = {
'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG}
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
log_level = levels[args.loglevel]
LOG.setLevel(log_level)
log_format = ("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]"
" %(message)s")
date_format = '%m/%d/%Y %I:%M:%S %p'
log_formatter = logging.Formatter(fmt=log_format,
datefmt=date_format)
fh = RotatingFileHandler('aprs-server.log',
maxBytes=(10248576 * 5),
backupCount=4)
log_format = "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]" " %(message)s"
date_format = "%m/%d/%Y %I:%M:%S %p"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler("aprs-server.log", maxBytes=(10248576 * 5), backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
@ -62,7 +53,6 @@ def setup_logging(args):
class MyAPRSTCPHandler(socketserver.BaseRequestHandler):
def handle(self):
# self.request is the TCP socket connected to the client
self.data = self.request.recv(1024).strip()
@ -81,8 +71,8 @@ def main():
CONFIG = utils.parse_config(args)
ip = CONFIG['aprs']['host']
port = CONFIG['aprs']['port']
ip = CONFIG["aprs"]["host"]
port = CONFIG["aprs"]["port"]
LOG.info("Start server listening on %s:%s" % (args.ip, args.port))
with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server:

View File

@ -19,37 +19,49 @@ import time
def fuzzy(hour, minute, degree=1):
'''Implements the fuzzy clock.
"""Implements the fuzzy clock.
returns the the string that spells out the time - hour:minute
Supports two degrees of fuzziness. Set with degree = 1 or degree = 2
When degree = 1, time is in quantum of 5 minutes.
When degree = 2, time is in quantum of 15 minutes.'''
When degree = 2, time is in quantum of 15 minutes."""
if degree <= 0 or degree > 2:
print('Please use a degree of 1 or 2. Using fuzziness degree=1')
print("Please use a degree of 1 or 2. Using fuzziness degree=1")
degree = 1
begin = 'It\'s '
begin = "It's "
f0 = 'almost '
f1 = 'exactly '
f2 = 'around '
f0 = "almost "
f1 = "exactly "
f2 = "around "
b0 = ' past '
b1 = ' to '
b0 = " past "
b1 = " to "
hourList = ('One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight',
'Nine', 'Ten', 'Eleven', 'Twelve')
hourlist = (
"One",
"Two",
"Three",
"Four",
"Five",
"Six",
"Seven",
"Eight",
"Nine",
"Ten",
"Eleven",
"Twelve",
)
s1 = s2 = s3 = s4 = ''
s1 = s2 = s3 = s4 = ""
base = 5
if degree == 1:
base = 5
val = ('Five', 'Ten', 'Quarter', 'Twenty', 'Twenty-Five', 'Half')
val = ("Five", "Ten", "Quarter", "Twenty", "Twenty-Five", "Half")
elif degree == 2:
base = 15
val = ('Quarter', 'Half')
val = ("Quarter", "Half")
# to find whether we have to use 'almost', 'exactly' or 'around'
dmin = minute % base
@ -74,20 +86,20 @@ def fuzzy(hour, minute, degree=1):
if minute <= base / 2:
# Case like "It's around/exactly Ten"
s2 = s3 = ''
s4 = hourList[hour - 12 - 1]
s2 = s3 = ""
s4 = hourlist[hour - 12 - 1]
elif minute >= 60 - base / 2:
# Case like "It's almost Ten"
s2 = s3 = ''
s4 = hourList[hour - 12]
s2 = s3 = ""
s4 = hourlist[hour - 12]
else:
# Other cases with all words, like "It's around Quarter past One"
if minute > 30:
s3 = b1 # to
s4 = hourList[hour - 12]
s4 = hourlist[hour - 12]
else:
s3 = b0 # past
s4 = hourList[hour - 12 - 1]
s4 = hourlist[hour - 12 - 1]
return begin + s1 + s2 + s3 + s4
@ -102,17 +114,17 @@ def main():
try:
deg = int(sys.argv[1])
except Exception:
print('Please use a degree of 1 or 2. Using fuzziness degree=1')
print("Please use a degree of 1 or 2. Using fuzziness degree=1")
if len(sys.argv) >= 3:
tm = sys.argv[2].split(':')
tm = sys.argv[2].split(":")
try:
h = int(tm[0])
m = int(tm[1])
if h < 0 or h > 23 or m < 0 or m > 59:
raise Exception
except Exception:
print('Bad time entered. Using the system time.')
print("Bad time entered. Using the system time.")
h = stm.tm_hour
m = stm.tm_min
print(fuzzy(h, m, deg))

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,44 @@
"""Utilities and helper functions."""
import logging
import errno
import os
import sys
import click
import yaml
# an example of what should be in the ~/.aprsd/config.yml
example_config = '''
ham:
callsign: KFART
DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "KFART"},
"aprs": {
"login": "someusername",
"password": "somepassword",
"host": "noam.aprs2.net",
"port": 14580,
"logfile": "/tmp/arsd.log",
},
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
"wb": "555309@vtext.com",
},
"smtp": {
"login": "something",
"password": "some lame password",
"host": "imap.gmail.com",
"port": 465,
"use_ssl": False,
},
"imap": {
"login": "imapuser",
"password": "something here too",
"host": "imap.gmail.com",
"port": 993,
"use_ssl": True,
},
}
aprs:
login: someusername
password: password
host: noam.aprs2.net
port: 14580
logfile: /tmp/aprsd.log
shortcuts:
'aa': '5551239999@vtext.com'
'cl': 'craiglamparter@somedomain.org'
'wb': '555309@vtext.com'
smtp:
login: something
password: some lame password
host: imap.gmail.com
port: 465
imap:
login: imapuser
password: something dumb
host: imap.gmail.com
'''
log = logging.getLogger('APRSD')
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
def env(*vars, **kwargs):
@ -45,20 +49,56 @@ def env(*vars, **kwargs):
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
return kwargs.get("default", "")
def get_config():
"""This tries to read the yaml config from ~/.aprsd/config.yml."""
config_file = os.path.expanduser("~/.aprsd/config.yml")
if os.path.exists(config_file):
with open(config_file, "r") as stream:
def mkdir_p(path):
"""Make directory and have it work in py2 and py3."""
try:
os.makedirs(path)
except OSError as exc: # Python >= 2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
def create_default_config():
"""Create a default config file."""
# make sure the directory location exists
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
config_dir = os.path.dirname(config_file_expanded)
if not os.path.exists(config_dir):
click.echo("Config dir '{}' doesn't exist, creating.".format(config_dir))
mkdir_p(config_dir)
with open(config_file_expanded, "w+") as cf:
yaml.dump(DEFAULT_CONFIG_DICT, cf)
def get_config(config_file):
"""This tries to read the yaml config from <config_file>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded, "r") as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return config
else:
log.critical("%s is missing, please create config file" % config_file)
print("\nCopy to ~/.aprsd/config.yml and edit\n\nSample config:\n %s"
% example_config)
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
"{} is missing, creating config file".format(config_file_expanded)
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = "Custom config file '{}' is missing.".format(config_file)
click.echo(msg)
sys.exit(-1)
@ -66,42 +106,55 @@ def get_config():
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config():
def parse_config(config_file):
# for now we still use globals....ugh
global CONFIG, LOG
global CONFIG
def fail(msg):
LOG.critical(msg)
click.echo(msg)
sys.exit(-1)
def check_option(config, section, name=None, default=None):
def check_option(config, section, name=None, default=None, default_fail=None):
if section in config:
if name and name not in config[section]:
if not default:
fail("'%s' was not in '%s' section of config file" %
(name, section))
fail(
"'%s' was not in '%s' section of config file" % (name, section)
)
else:
config[section][name] = default
else:
if (
default_fail
and name in config[section]
and config[section][name] == default_fail
):
# We have to fail and bail if the user hasn't edited
# this config option.
fail("Config file needs to be edited from provided defaults.")
else:
fail("'%s' section wasn't in config file" % section)
return config
# Now read the ~/.aprds/config.yml
config = get_config()
check_option(config, 'shortcuts')
check_option(config, 'ham', 'callsign')
check_option(config, 'aprs', 'login')
check_option(config, 'aprs', 'password')
check_option(config, 'aprs', 'host')
check_option(config, 'aprs', 'port')
config = check_option(config, 'aprs', 'logfile', './aprsd.log')
check_option(config, 'imap', 'host')
check_option(config, 'imap', 'login')
check_option(config, 'imap', 'password')
check_option(config, 'smtp', 'host')
check_option(config, 'smtp', 'port')
check_option(config, 'smtp', 'login')
check_option(config, 'smtp', 'password')
config = get_config(config_file)
check_option(config, "shortcuts")
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config, "ham", "callsign", default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"]
)
check_option(config, "aprs", "login")
check_option(config, "aprs", "password")
check_option(config, "aprs", "host")
check_option(config, "aprs", "port")
check_option(config, "aprs", "logfile", "./aprsd.log")
check_option(config, "imap", "host")
check_option(config, "imap", "login")
check_option(config, "imap", "password")
check_option(config, "smtp", "host")
check_option(config, "smtp", "port")
check_option(config, "smtp", "login")
check_option(config, "smtp", "password")
return config
LOG.info("aprsd config loaded")

9
dev-requirements.in Normal file
View File

@ -0,0 +1,9 @@
tox
pytest
pytest-cov
mypy
flake8
pep8-naming
black
isort
Sphinx

60
dev-requirements.txt Normal file
View File

@ -0,0 +1,60 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile dev-requirements.in
#
alabaster==0.7.12 # via sphinx
appdirs==1.4.4 # via black, virtualenv
attrs==20.3.0 # via pytest
babel==2.9.0 # via sphinx
black==20.8b1 # via -r dev-requirements.in
certifi==2020.12.5 # via requests
chardet==3.0.4 # via requests
coverage==5.3 # via pytest-cov
distlib==0.3.1 # via virtualenv
docutils==0.16 # via sphinx
filelock==3.0.12 # via tox, virtualenv
flake8-polyfill==1.0.2 # via pep8-naming
flake8==3.8.4 # via -r dev-requirements.in, flake8-polyfill
idna==2.10 # via requests
imagesize==1.2.0 # via sphinx
iniconfig==1.1.1 # via pytest
isort==5.6.4 # via -r dev-requirements.in
jinja2==2.11.2 # via sphinx
markupsafe==1.1.1 # via jinja2
mccabe==0.6.1 # via flake8
mypy-extensions==0.4.3 # via black, mypy
mypy==0.790 # via -r dev-requirements.in
packaging==20.7 # via pytest, sphinx, tox
pathspec==0.8.1 # via black
pep8-naming==0.11.1 # via -r dev-requirements.in
pluggy==0.13.1 # via pytest, tox
py==1.9.0 # via pytest, tox
pycodestyle==2.6.0 # via flake8
pyflakes==2.2.0 # via flake8
pygments==2.7.3 # via sphinx
pyparsing==2.4.7 # via packaging
pytest-cov==2.10.1 # via -r dev-requirements.in
pytest==6.1.2 # via -r dev-requirements.in, pytest-cov
pytz==2020.4 # via babel
regex==2020.11.13 # via black
requests==2.25.0 # via sphinx
six==1.15.0 # via tox, virtualenv
snowballstemmer==2.0.0 # via sphinx
sphinx==3.3.1 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.2 # via sphinx
sphinxcontrib-devhelp==1.0.2 # via sphinx
sphinxcontrib-htmlhelp==1.0.3 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx
sphinxcontrib-serializinghtml==1.1.4 # via sphinx
toml==0.10.2 # via black, pytest, tox
tox==3.20.1 # via -r dev-requirements.in
typed-ast==1.4.1 # via black, mypy
typing-extensions==3.7.4.3 # via black, mypy
urllib3==1.26.2 # via requests
virtualenv==20.2.2 # via tox
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View File

@ -4,3 +4,4 @@ imapclient
pbr
pyyaml
six
requests

View File

@ -24,6 +24,4 @@ try:
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)
setuptools.setup(setup_requires=["pbr"], pbr=True)

View File

@ -0,0 +1,3 @@
flake8
pytest
mock

0
tests/__init__.py Normal file
View File

23
tests/test_main.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import sys
import unittest
import pytest
from aprsd import main
if sys.version_info >= (3, 2):
from unittest import mock
else:
import mock
class testMain(unittest.TestCase):
@mock.patch("aprsd.main._imap_connect")
@mock.patch("aprsd.main._smtp_connect")
def test_validate_email(self, imap_mock, smtp_mock):
"""Test to make sure we fail."""
imap_mock.return_value = None
smtp_mock.return_value = {"smaiof": "fire"}
main.validate_email()

89
tox.ini
View File

@ -1,26 +1,51 @@
[tox]
minversion = 1.6
minversion = 2.9.0
skipdist = True
envlist = py27,py36,py37,fast8,pep8,cover,docs
skip_missing_interpreters = true
envlist = py{27,36,37,38},pep8,fmt-check
# Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and
# arguments use the pyproject.toml file as specified in PEP-517 and PEP-518.
isolated_build = true
[testenv]
setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install {opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/dev-requirements.txt
commands =
pytest aprsd/main.py {posargs}
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
# Use -Werror to treat warnings as errors.
# {envpython} -bb -Werror -m pytest \
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
{envpython} -bb -Werror -m pytest {posargs}
[testenv:cover]
[testenv:py27]
setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install {opts} {packages}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements-py2.txt
commands =
pytest --cov=aprsd
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
# Use -Werror to treat warnings as errors.
# {envpython} -bb -Werror -m pytest \
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
{envpython} -bb -Werror -m pytest
[testenv:docs]
deps = -r{toxinidir}/test-requirements.txt
commands = sphinx-build -b html docs/source docs/html
[testenv:pep8-27]
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements-py2.txt
commands =
flake8 {posargs} aprsd
[testenv:pep8]
commands =
flake8 {posargs} aprsd
@ -33,7 +58,57 @@ commands =
{toxinidir}/tools/fast8.sh
passenv = FAST8_NUM_COMMITS
[testenv:lint]
skip_install = true
deps =
-r{toxinidir}/dev-requirements.txt
commands =
flake8 aprsd
[flake8]
max-line-length = 99
show-source = True
ignore = E713,E501
ignore = E713,E501,W503
extend-ignore = E203,W503
extend-exclude = venv
exclude = .venv,.git,.tox,dist,doc,.ropeproject
# This is the configuration for the tox-gh-actions plugin for GitHub Actions
# https://github.com/ymyzk/tox-gh-actions
# This section is not needed if not using GitHub Actions for CI.
[gh-actions]
python =
2.7: py27, pep8-27
3.6: py36, pep8, fmt-check
3.7: py38, pep8, fmt-check
3.8: py38, pep8, fmt-check, type-check, docs
3.9: py39
[testenv:fmt]
# This will reformat your code to comply with pep8
# and standard formatting
skip_install = true
deps =
-r{toxinidir}/dev-requirements.txt
commands =
isort .
black .
[testenv:fmt-check]
# Runs a check only on code formatting.
# you can fix imports by running isort standalone
# you can fix code formatting by running black standalone
skip_install = true
deps =
-r{toxinidir}/dev-requirements.txt
commands =
isort --check-only .
black --check .
[testenv:type-check]
skip_install = true
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt
commands =
mypy aprsd