Skip to content

Commit

Permalink
swc-installation-test-2.py: Add remote check configuration
Browse files Browse the repository at this point in the history
On Wed, Mar 11, 2015 at 03:08:15PM -0700, Piotr Banaszkiewicz wrote [1]:
> Maybe add workshop-template/requirements.txt (not necessarily a
> Python dependencies file) that's read by installation testing script
> and adjusts CHECKS entries accordingly?

The old CHECKS approach didn't work very well, because many
instructors would forget to edit CHECKS and learners would be confused
when they failed checks for packages that they didn't actually need
(but which were still listed in CHECKS) [2,3,4,5].  This commit makes:

  $ ./swc-installation-test-2.py

a no-op.  Users will either have to explicitly specify checks on the
command line:

  $ ./swc-installation-test-2.py virtual-editor

or point the tester at an instructor-provided config:

  $ ./swc-installation-test-2.py --url https://swcarpentry.github.io/2015-11-09-abc/lessons.json

Because dependencies for a given lesson can be tricky and are shared
by all lesson consumers, it offloads the bulk of the
requirement-listing to lesson maintainers.  Once that work is done,
instructors can just list out the lessons they're covering (which is
also useful for tools like AMY [6] who would like an automatic way to
determine what is being taught).

SWC generally prefers YAML to JSON, but I've gone with JSON here to
stick with stock Python 2.6+ support.  I've also changed some
or_dependency definitions from tuples to lists so I can mutate them
with .remove().

[1]: numfocus/gsoc#3 (comment)
[2]: carpentries/workshop-template#136
[3]: carpentries/workshop-template#180
[4]: carpentries/workshop-template#181
[5]: carpentries/workshop-template#258
[6]: https://github.com/swcarpentry/amy
  • Loading branch information
wking committed Nov 14, 2015
1 parent 5e48833 commit 937e541
Show file tree
Hide file tree
Showing 2 changed files with 180 additions and 86 deletions.
107 changes: 79 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ For learners
============

This directory contains scripts for testing your machine to make sure
you have the software you'll need for your workshop installed. See
the comments at the head of each script for more details, but you'll
basically want to see something like:
you have the software you'll need for your workshop installed. Your
instructors should provide you with a URL listing the workshop's
dependencies. See the comments at the head of each script for more
details, but you'll basically want to see something like:

$ python swc-installation-test-1.py
Passed
$ python swc-installation-test-2.py
$ python swc-installation-test-2.py --url https://swcarpentry.github.io/2015-11-09-abc/lessons.json
check virtual-shell... pass
Successes:
Expand All @@ -18,7 +19,7 @@ basically want to see something like:

If you see something like:

$ python swc-installation-test-2.py
$ python swc-installation-test-2.py --url https://swcarpentry.github.io/2015-11-09-abc/lessons.json
check virtual-shell... fail
check for command line shell (virtual-shell) failed:
Expand All @@ -36,7 +37,7 @@ follow the suggestions to try and install any missing software. For
additional troubleshooting information, you can use the `--verbose`
option:

$ python swc-installation-test-2.py --verbose
$ python swc-installation-test-2.py --url https://swcarpentry.github.io/2015-11-09-abc/lessons.json --verbose
check virtual-shell... fail
==================
Expand All @@ -51,26 +52,76 @@ For instructors
`swc-installation-test-1.py` is pretty simple, and just checks that
the students have a recent enough version of Python installed that
they'll be able to parse `swc-installation-test-2.py`. The latter
checks for a list of dependencies and prints error messages if a
checks for your workshop's requirements and prints error messages if a
package is not installed, or if the installed version is not current
enough. By default, the script checks for pretty much anything that
has ever been used at a Software Carpentry workshop, which is probably
not what you want for your particular workshop.

Before your workshop, you should go through
`swc-installation-test-2.py` and comment any dependencies you don't
need out of the `CHECKS` list. You might also want to skim through
the minimum version numbers listed where particular dependencies are
defined (e.g. `('git', 'Git', (1, 7, 0), None)`). For the most part,
fairly conservative values have been selected, so students with modern
machines should be fine. If your workshop has stricter version
requirements, feel free to bump them accordingly.

Similarly, the virtual dependencies can be satisfied by any of several
packages. If you don't want to support a particular package (e.g. if
you have no Emacs experience and don't want to be responsible for
students who show up with Emacs as their only editor), you can comment
out that particular `or_dependency`.

Finally, don't forget to post your modified scripts somewhere where
your students can download them!
enough. When you setup your workshop, you should write a [JSON][]
file listing the lessons you will cover. For example:

{
"shell": {
"requirements": "https://github.com/swcarpentry/shell-novice/blob/v5.4/requirements.json"
},
"git": {
"requirements": "https://github.com/swcarpentry/git-novice/blob/v5.3/requirements.json"
}
"python": {
"requirements": "https://github.com/swcarpentry/python-novice-inflammation/blob/v5.4/requirements.json"
},
"sql": {
"requirements": "https://github.com/swcarpentry/sql-novice-survey/blob/v5.7/requirements.json"
}
}

The lesson names (“shell”, “git”, …) are only used for logging, and
you may add other attributes or entries that don't define
`requirements` if you wish. `swc-installation-test-2.py` will collect
requirements from the lesson requirement files and then check to make
sure they are all satisfied. If you want to use a lesson that does
not provide a `requirements.json`, you can write your own requirement
file for that lesson and use your URL in your workshop's JSON:

{
"shell": {
"requirements": "https://swcarpentry.github.io/2015-11-09-abc/shell-requirements.json"
},
}


For lesson maintainers
======================

By providing a `requirements.json` file, you make it easy for others
to discover its requirements. For example, [instructors can write
workshop requirements](#for-instructors) referencing your listing.
The [JSON][] file should be an object whose keys are check names and
whose values are objects that may contain a `minimum-version` key. If
`minimum-version` is set, it must be an array specifying the minimum
version of the checked software compatible with your lesson. For
example:

{
"git": {
"minimum-version": [1, 7, 0]
},
"virtual-editor": {},
"virtual-browser": {},
"virtual-shell": {}
}

You may add other attributes to the per-check objects if you wish.

Virtual dependencies can be satisfied by any of several packages. If
you don't want to support a particular package (e.g. if you have no
Emacs experience and don't want to be responsible for students who
show up with Emacs as their only editor), you can blacklist the
package:

{
"virtual-editor": {
"blacklist": ["emacs"]
},
}

[JSON]: http://json.org/
159 changes: 101 additions & 58 deletions swc-installation-test-2.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
This script requires at least Python 2.6. You can check the version
of Python that you have installed with 'swc-installation-test-1.py'.
By default, this script will test for all the dependencies your
instructor thinks you need. If you want to test for a different set
of packages, you can list them on the command line. For example:
To test for a particular event's requirements, point the script at a
lesson listing:
python swc-installation-test-2.py --url https://swcarpentry.github.io/2015-11-09-abc/lessons.json
replacing the URL with your event's lesson listing. If you want to
test for a different set of packages, you can list them on the command
line. For example:
python swc-installation-test-2.py git virtual-editor
Expand All @@ -28,9 +33,7 @@
# Dependency class. You can refer to the code to see which package
# comes under which type of dependency.

# The CHECKER dictionary stores information about all the dependencies
# and CHECKS stores list of the dependencies which are to be checked in
# the current workshop.
# The CHECKER dictionary stores information about all the dependencies.

# In the "__name__ == '__main__'" block, we launch all the checks with
# check() function, which prints information about the tests as they run
Expand Down Expand Up @@ -58,13 +61,18 @@ def import_module(name):
module = getattr(module, n)
return module
_importlib = _Importlib()
import json as _json
import logging as _logging
import os as _os
import platform as _platform
import re as _re
import shlex as _shlex
import subprocess as _subprocess
import sys as _sys
try: # Python 3.x
import urllib.request as _urllib_request
except ImportError: # Python 2.x
import urllib2 as _urllib_request # for urlopen()
import xml.etree.ElementTree as _element_tree


Expand All @@ -76,47 +84,6 @@ def import_module(name):

__version__ = '0.1'

# Comment out any entries you don't need
CHECKS = [
# Shell
'virtual-shell',
# Editors
'virtual-editor',
# Browsers
'virtual-browser',
# Version control
'git',
'hg', # Command line tool
#'mercurial', # Python package
'EasyMercurial',
# Build tools and packaging
'make',
'virtual-pypi-installer',
'setuptools',
#'xcode',
# Testing
'nosetests', # Command line tool
'nose', # Python package
'py.test', # Command line tool
'pytest', # Python package
# SQL
'sqlite3', # Command line tool
'sqlite3-python', # Python package
# Python
'python',
'ipython', # Command line tool
'IPython', # Python package
'argparse', # Useful for utility scripts
'numpy',
'scipy',
'matplotlib',
'pandas',
#'sympy',
#'Cython',
#'networkx',
#'mayavi.mlab',
]

CHECKER = {}

_ROOT_PATH = _os.sep
Expand Down Expand Up @@ -246,11 +213,9 @@ def __str__(self):
return '\n'.join(lines)


def check(checks=None):
def check(checks):
successes = []
failures = []
if not checks:
checks = CHECKS
for check in checks:
try:
checker = CHECKER[check]
Expand Down Expand Up @@ -876,7 +841,7 @@ def _program_files_paths(*args):


for name,long_name,dependencies in [
('virtual-shell', 'command line shell', (
('virtual-shell', 'command line shell', [
'bash',
'dash',
'ash',
Expand All @@ -885,8 +850,8 @@ def _program_files_paths(*args):
'csh',
'tcsh',
'sh',
)),
('virtual-editor', 'text/code editor', (
]),
('virtual-editor', 'text/code editor', [
'emacs',
'xemacs',
'vim',
Expand All @@ -899,23 +864,91 @@ def _program_files_paths(*args):
'textmate',
'textwrangler',
'other-editor', # last because it requires user interaction
)),
('virtual-browser', 'web browser', (
]),
('virtual-browser', 'web browser', [
'firefox',
'google-chrome',
'chromium',
'safari',
)),
('virtual-pypi-installer', 'PyPI installer', (
]),
('virtual-pypi-installer', 'PyPI installer', [
'pip',
'easy_install',
)),
]),
]:
CHECKER[name] = VirtualDependency(
name=name, long_name=long_name, or_dependencies=dependencies)
del name, long_name, dependencies # cleanup namespace


def _get_json(url):
response = _urllib_request.urlopen(url) # not context manager in Python 2
info = response.info()
response_bytes = response.read()
if hasattr(info, 'get_content_charset'):
# Python 3, info is email.message.Message
charset = info.get_content_charset('UTF-8')
else: # Python 2, info is mimetools.Message
charset = info.getparam('charset') or 'UTF-8'
response.close()
json = response_bytes.decode(charset)
return _json.loads(json)


def _update_checker(url, checker, config):
if 'minimum-version' in config:
version = config['minimum-version']
if (not checker.minimum_version or
version > checker.minimum_version):
_LOG.debug('set minimum version for {} to {}'.format(
checker.name, version))
checker.minimum_version = version
if 'blacklist' in config:
_LOG.debug('blacklist or-dependencies for {}: {}'.format(
checker.name, config['blacklist']))
for check in config['blacklist']:
try:
checker.or_dependencies.remove(check)
except ValueError:
try:
c = CHECKER[check]
except KeyError as e:
_LOG.warning(
'{} blacklists an unrecognized check: {}'.format(
url, requirement))
raise InvalidCheck(check)# from e
try:
checker.or_dependencies.remove(c)
except ValueError:
_LOG.debug(
'not blacklisting missing or-dependency for {}: {}'
.format(checker.name, check))
pass # maybe someone else already removed it


def get_checks(url):
checks = set()
_LOG.debug('get lessons from {}'.format(url))
lessons = _get_json(url=url)
for lesson, lesson_config in lessons.items():
requirements_url = lesson_config.get('requirements')
if requirements_url:
_LOG.debug('get requirements for {} from {}'.format(
lesson, requirements_url))
requirements = _get_json(url=requirements_url)
for check, config in requirements.items():
checks.add(check)
try:
checker = CHECKER[check]
except KeyError as e:
_LOG.warning(
'{} requires an unrecognized check: {}'.format(
requirements_url, check))
raise InvalidCheck(check)# from e
_update_checker(url=requirements_url, checker=checker, config=config)
return sorted(checks)


def _print_info(key, value, indent=19):
print('{0}{1}: {2}'.format(key, ' '*(indent-len(key)), value))

Expand Down Expand Up @@ -977,6 +1010,9 @@ def print_suggestions(instructor_fallback=True):
'-v', '--verbose', action='store_true',
help=('print additional information to help troubleshoot '
'installation issues'))
parser.add_option(
'-u', '--url', metavar='URL',
help='URL for the workshop repository or GitHub pages website')
options,args = parser.parse_args()

if options.verbose:
Expand All @@ -989,6 +1025,13 @@ def print_suggestions(instructor_fallback=True):
print(key)
_sys.exit(0)

if options.url:
if args:
print(
'you should set either --url or explicit checks, not both',
file=_sys.stderr)
_sys.exit(1)
args = get_checks(url=options.url)
try:
passed = check(args)
except InvalidCheck as e:
Expand Down

0 comments on commit 937e541

Please sign in to comment.