From 59c8d8a495ab502f80dd23ec7a208b0bfb9d3401 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Mon, 2 Sep 2019 11:09:21 +0100 Subject: [PATCH] Project docs, scaffolding, utilities and other useful stuff. See README for details. --- .gitignore | 107 +++++++++++++++++++++++++++++++++++++++++++ CHANGES.rst | 9 ++++ CONTRIBUTING.rst | 20 ++++++++ Makefile | 67 +++++++++++++++++++++++++++ README.rst | 61 +++++++++++++++++++++++- circup.py | 60 ++++++++++++++++++++++++ docs/Makefile | 20 ++++++++ docs/conf.py | 61 ++++++++++++++++++++++++ docs/index.rst | 21 +++++++++ docs/make.bat | 35 ++++++++++++++ setup.py | 83 +++++++++++++++++++++++++++++++++ tests/__init__.py | 0 tests/test_circup.py | 60 ++++++++++++++++++++++++ 13 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CHANGES.rst create mode 100644 CONTRIBUTING.rst create mode 100644 Makefile create mode 100644 circup.py create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_circup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4547bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# vim +*.swp diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..7d509cd --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,9 @@ +Release History +=============== + +0.0.1 +----- + +Initial release. + +* TO BE DONE. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..4a0d6cd --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,20 @@ +Contributing +============ + +Please note that this project is released with a Contributor Code of Conduct. +By participating in this project you agree to abide by its terms. Participation +covers any forum used to converse about CircuitPython including unofficial and +official spaces. Failure to do so will result in corrective actions such as +time out or ban from the project. + +Licensing +--------- + +By contributing to this repository you are certifying that you have all +necessary permissions to license the code under an MIT License. You still +retain the copyright but are granting many permissions under the MIT License. + +If you have an employment contract with your employer please make sure that +they don't automatically own your work product. Make sure to get any necessary +approvals before contributing. Another term for this contribution off-hours is +moonlighting. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b725184 --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +XARGS := xargs -0 $(shell test $$(uname) = Linux && echo -r) +GREP_T_FLAG := $(shell test $$(uname) = Linux && echo -T) + +all: + @echo "\nThere is no default Makefile target right now. Try:\n" + @echo "make clean - reset the project and remove auto-generated assets." + @echo "make pyflakes - run the PyFlakes code checker." + @echo "make pycodestyle - run the PEP8 style checker." + @echo "make test - run the test suite." + @echo "make coverage - view a report on test coverage." + @echo "make tidy - tidy code with the 'black' formatter." + @echo "make check - run all the checkers and tests." + @echo "make dist - make a dist/wheel for the project." + @echo "make publish-test - publish the project to PyPI test instance." + @echo "make publish-live - publish the project to PyPI production." + @echo "make docs - run sphinx to create project documentation.\n" + +clean: + rm -rf build + rm -rf dist + rm -rf circup.egg-info + rm -rf .coverage + rm -rf .eggs + rm -rf .pytest_cache + rm -rf .tox + rm -rf docs/_build + find . \( -name '*.py[co]' -o -name dropin.cache \) -delete + find . \( -name '*.bak' -o -name dropin.cache \) -delete + find . \( -name '*.tgz' -o -name dropin.cache \) -delete + find . | grep -E "(__pycache__)" | xargs rm -rf + +pyflakes: + find . \( -name _build -o -name var -o -path ./docs \) -type d -prune -o -name '*.py' -print0 | $(XARGS) pyflakes + +pycodestyle: + find . \( -name _build -o -name var \) -type d -prune -o -name '*.py' -print0 | $(XARGS) -n 1 pycodestyle --repeat --exclude=docs/*,.vscode/* --ignore=E731,E402,W504,W503 + +test: clean + pytest --random-order + +coverage: clean + pytest --random-order --cov-config .coveragerc --cov-report term-missing --cov=circup tests/ + +tidy: clean + @echo "\nTidying code with black..." + black -l 79 circup.py + black -l 79 tests + +check: clean tidy pycodestyle pyflakes coverage + +dist: check + @echo "\nChecks pass, good to package..." + python setup.py sdist bdist_wheel + +publish-test: dist + @echo "\nPackaging complete... Uploading to PyPi..." + twine upload -r test --sign dist/* + +publish-live: dist + @echo "\nPackaging complete... Uploading to PyPi..." + twine upload --sign dist/* + +docs: clean + $(MAKE) -C docs html + @echo "\nDocumentation can be found here:" + @echo file://`pwd`/docs/_build/html/index.html + @echo "\n" diff --git a/README.rst b/README.rst index b8e981f..af80212 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,63 @@ CircUp ====== -A tool to update CircuitPython libraries. More soon... +A tool to manage and update libraries on a CircuitPython device. + +How +--- + +Each CircuitPython library on the device (``.py``, *NOT* ``.mpy`` at this time) +has a version number and a github repo URL. + +This utility looks at all the libraries on the device and checks if they are +the most recet (compared to what is in the referenced GitHub repository). If +the libraries are out of date, the utility downloads them to the local device +and/or local system in a zip file. + +Example libraries: + +https://github.com/adafruit/Adafruit_CircuitPython_Bundle/releases/download/20190830/adafruit-circuitpython-bundle-py-20190830.zip + +Usage +----- + +Example usage:: + + circup list --outdated + + Package Version Latest + ----------- ------- ------ + foo 1.0.1 1.1.0 + bar 19.3 19.4 + baz 0.3.1 0.9 + +Developer Setup +--------------- + +Clone the repository then make a virtualenv. From the root of the project, +install the requirements:: + + pip install -r ".[dev]" + +Run the test suite:: + + make check + +There is a Makefile that helps with most of the common workflows associated +with development. Typing "make" on its own will list the options thus:: + + $ make + + There is no default Makefile target right now. Try: + + make clean - reset the project and remove auto-generated assets. + make pyflakes - run the PyFlakes code checker. + make pycodestyle - run the PEP8 style checker. + make test - run the test suite. + make coverage - view a report on test coverage. + make tidy - tidy code with the 'black' formatter. + make check - run all the checkers and tests. + make dist - make a dist/wheel for the project. + make publish-test - publish the project to PyPI test instance. + make publish-live - publish the project to PyPI production. + make docs - run sphinx to create project documentation. diff --git a/circup.py b/circup.py new file mode 100644 index 0000000..e28de31 --- /dev/null +++ b/circup.py @@ -0,0 +1,60 @@ +""" +CircUp -- a utility to manage and update libraries on a CircuitPython device. + +Copyright (c) 2019 Adafruit Industries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from serial.tools.list_ports import comports as list_serial_ports + + +# IMPORTANT +# --------- +# Keep these metadata assignments simple and single-line. They are parsed +# somewhat naively by setup.py. +__title__ = "circup" +__description__ = "A tool to manage/update libraries on CircuitPython devices." +__version__ = "0.0.1" +__license__ = "MIT" +__url__ = "https://github.com/adafruit/circup" +__author__ = "Adafruit Industries" +__email__ = "ntoll@ntoll.org" + + +VENDOR_ID = 9114 #: The unique USB vendor ID for Adafruit boards. + + +def find_device(): + """ + Returns a tuple containing the port's device and description for a + connected Adafruit device. If no device is connected, the tuple will be + (None, None). + """ + ports = list_serial_ports() + for port in ports: + if port.vid == VENDOR_ID: + return (port.device, port.description) + return (None, None) + + +def main(): # pragma: no cover + """ + TODO: Finish this. Just checking things work. + """ + print(find_device()) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..ac1f783 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,61 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'CircUp' +copyright = '2019, Adafruit Industries' +author = 'Adafruit Industries' + +# The full version, including alpha/beta/rc tags +import circup +release = circup.__version__ + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..efecbe0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +.. CircUp documentation master file, created by + sphinx-quickstart on Mon Sep 2 10:58:36 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. include:: ../README.rst + +.. include:: ../CONTRIBUTING.rst + +API +=== + +.. automodule:: circup + :members: + +.. include:: ../CHANGES.rst + +License +======= + +.. include:: ../LICENSE diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1aeeaa6 --- /dev/null +++ b/setup.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import os +import re +from setuptools import setup + + +base_dir = os.path.dirname(__file__) + + +DUNDER_ASSIGN_RE = re.compile(r"""^__\w+__\s*=\s*['"].+['"]$""") +about = {} +with open(os.path.join(base_dir, "circup.py"), encoding="utf8") as f: + for line in f: + if DUNDER_ASSIGN_RE.search(line): + exec(line, about) + + +with open(os.path.join(base_dir, "README.rst"), encoding="utf8") as f: + readme = f.read() + +with open(os.path.join(base_dir, "CHANGES.rst"), encoding="utf8") as f: + changes = f.read() + + +install_requires = ["pyserial>=3.0,<4.0", "PyGithub>=1.43.8"] + +extras_require = { + "tests": [ + "pytest", + "pytest-cov", + "pytest-random-order>=1.0.0", + "pytest-faulthandler", + "coverage", + "pycodestyle", + "pyflakes", + "black", + ], + "docs": ["sphinx"], + "package": [ + # Wheel building and PyPI uploading + "wheel", + "twine", + ], +} + +extras_require["dev"] = ( + extras_require["tests"] + + extras_require["docs"] + + extras_require["package"] +) + +extras_require["all"] = list( + {req for extra, reqs in extras_require.items() for req in reqs} +) + +setup( + name=about["__title__"], + version=about["__version__"], + description=about["__description__"], + long_description="{}\n\n{}".format(readme, changes), + author=about["__author__"], + author_email=about["__email__"], + url=about["__url__"], + license=about["__license__"], + py_modules=["circup"], + install_requires=install_requires, + extras_require=extras_require, + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Education", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Software Distribution", + ], + entry_points={"console_scripts": ["circup=circup:main"]}, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_circup.py b/tests/test_circup.py new file mode 100644 index 0000000..78f0141 --- /dev/null +++ b/tests/test_circup.py @@ -0,0 +1,60 @@ +""" +Unit tests for the circup module. + +Copyright (c) 2019 Adafruit Industries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +import circup +from unittest import mock + + +def test_find_device(): + """ + Ensure the find_device function returns the expected information about + any Adafruit devices connected to the user's computer. + """ + + class FakePort: + """ + Pretends to be a representation of a port in PySerial. + """ + + def __init__(self, vid, device, description): + self.vid = vid + self.device = device + self.description = description + + device = "/dev/ttyACM3" + description = "CircuitPlayground Express - CircuitPython CDC control" + port = FakePort(circup.VENDOR_ID, device, description) + ports = [port] + with mock.patch("circup.list_serial_ports", return_value=ports): + result = circup.find_device() + assert result == (device, description) + + +def test_find_device_not_connected(): + """ + If no Adafruit device is connected to the user's computer, ensure the + result is (None, None) + """ + with mock.patch("circup.list_serial_ports", return_value=[]): + result = circup.find_device() + assert result == (None, None)