Permalink
Browse files

Updating documentation and adding tests for variable substitution

  • Loading branch information...
1 parent cdb4e85 commit e2159640efc9e3568e7d1c96f1c46c25ad1ff756 @davidjb davidjb committed Jul 16, 2012
Showing with 322 additions and 38 deletions.
  1. +156 −23 README.rst
  2. +121 −0 bootstrap.py
  3. +11 −0 buildout.cfg
  4. +26 −15 githubcollective/config.py
  5. +5 −0 setup.cfg
  6. +3 −0 setup.py
View
179 README.rst
@@ -89,43 +89,162 @@ Variable Substitution
the form ``${my-section:option}``, which will automatically resolve to the value
of ``option`` within the ``[my-section]`` section of your configuration.
-As an illustrated example::
-
+Here is our example configuration:
+
+ >>> configuration = """
+ ... [config]
+ ... my-domain = example.org
+ ... my-url = http://${:my-domain}/
+ ...
+ ... [repo:my-repo]
+ ... owners = ${:main-user}
+ ... homepage = ${config:my-url}
+ ... hooks = travis-ci
+ ... main-user = my-user
+ ... travis-user = ${:owners}
+ ... travis-ci-token = b8cd21c6317a51eeaa752802a0c04454
+ ...
+ ... [hook:travis-ci]
+ ... name = travis
+ ... config =
+ ... {
+ ... "user": "${repo:travis-user}",
+ ... "token": "${repo:travis-ci-token}"
+ ... }
+ ... events = push
+ ... active = true
+ ... """
+
+We can load this configuration to see the result:
+
+ >>> import ConfigParser
+ >>> import StringIO
+ >>> from githubcollective.config import substitute, global_substitute
+
+ >>> config = ConfigParser.SafeConfigParser()
+ >>> config.readfp(StringIO.StringIO(configuration))
+ >>> global_substitute(config)
+
+ >>> result = StringIO.StringIO()
+ >>> config.write(result)
+ >>> result.seek(0)
+
+Which, after global substitution is applied, will look like the following.
+Note that there are still some substitutions present - these are `Local`
+subsitutions and will be resolved in a `context` (in this case a repository
+context for the given hook options) when the revelant context is being
+interpreted.
+
+ >>> print result.read().replace('\t', ' ')
[config]
+ my-domain = example.org
my-url = http://example.org/
-
+ <BLANKLINE>
[repo:my-repo]
- owners = ${:main-user}
- homepage = ${config:my-url}
+ owners = my-user
+ homepage = http://example.org/
hooks = travis-ci
- hooks-events = push
- main-user = davidjb
- travis-user = ${:owners}
+ main-user = my-user
+ travis-user = my-user
travis-ci-token = b8cd21c6317a51eeaa752802a0c04454
-
+ <BLANKLINE>
[hook:travis-ci]
name = travis
config =
{
"user": "${repo:travis-user}",
"token": "${repo:travis-ci-token}"
}
- events = ${repo:my-repo:hooks-events}
+ events = push
active = true
+ <BLANKLINE>
+ <BLANKLINE>
+
+We can now test our substitution functionality using this configuration
+as follows. We'll test this by re-initialising the original configuration
+before it had global subsitution applied.
-In the above example, we demonstrate all types of substitution:
+ >>> config = ConfigParser.SafeConfigParser()
+ >>> config.readfp(StringIO.StringIO(configuration))
+
+In the above example, we demonstrate all types of substitution, including
+substitutions that refer to other substitutions and ensure that these all
+can be resolved successfully.
-`Global`
- ``${config:my-url}`` and ``${repo:my-repo:hooks-events}``, which refers to
- a fully-qualified section and option.
-`Same section`
- ``${:main-user}``, which refers to an option in the same section.
-`Local`
- ``${repo:travis-user}``, which refers to a local option that is resolved
- at the time relevant section is processed, in the appropriate context.
- At present, hooks are the only things that belong to repositories, so
- attempting to use such a field in anything other than a ``[hook:]``
- context will not work.
+Global options
+^^^^^^^^^^^^^^
+
+These options look like ``${config:my-url}`` and
+``${repo:my-repo:hooks-events}``, which refers to a fully-qualified section and
+option.
+
+For example, using the configuration above, you are able to refer to options
+like so:
+
+ >>> substitute('${config:my-domain}', config)
+ 'example.org'
+
+ >>> substitute('${config:my-url}', config)
+ 'http://example.org/'
+
+ >>> substitute('${repo:my-repo:main-user}', config)
+ 'my-user'
+
+ >>> substitute('${hook:travis-ci:name}', config)
+ 'travis'
+
+If you attempt to refer to a missing option or section, you'll be informed
+of this:
+
+ >>> substitute('${config:idontexist}', config)
+ ... # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ NoOptionError: No option 'idontexist' in section: 'config'
+
+ >>> substitute('${idontexist:option}', config)
+ ... # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ NoSectionError: No section: 'idontexist'
+
+
+Options in same section
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Substitution can refer to another option within the same section by omitting
+the section name like so: ``${:main-user}``.
+
+Using the example configuration above, we see we can resolve options with
+a given context:
+
+ >>> substitute('${:main-user}', config, context='repo:my-repo')
+ 'my-user'
+
+ >>> substitute('${:events}', config, context='hook:travis-ci')
+ 'push'
+
+Local options
+^^^^^^^^^^^^^
+
+These are special options that look like ``${repo:travis-user}``, which refers
+to a local option that is resolved at the time relevant section is processed,
+in the appropriate context. At present, hooks are the only things that belong
+to repositories, so attempting to use such a field in anything other than a
+``[hook:]`` context will not work.
+
+For example:
+
+ >>> substitute('${repo:travis-user}', config,
+ ... context='repo:my-repo', local=True)
+ 'my-user'
+
+ >>> substitute('${repo:travis-ci-token}', config,
+ ... context='repo:my-repo', local=True)
+ 'b8cd21c6317a51eeaa752802a0c04454'
+
+Ordering and options
+^^^^^^^^^^^^^^^^^^^^
Options are resolved top-to-bottom within the configuration, with the exception
of `Local` options that are resolved when instantiated (for instance,
@@ -525,11 +644,25 @@ Cached configuration
-u garbas \ # account that has management right for organization
-P PASSWORD # account password
+Testing
+=======
+
+``nose`` is utilised for testing and configuration for ``nose`` exists
+within the ``setup.cfg`` file within this project. This configuration
+automatically examines files for tests within the project, including
+this read-me itself. You can initialise and run tests using the Buildout
+configuration provided::
+
+ git clone git://github.com/collective/github-collective.git
+ cd github-collective
+ virtualenv .
+ python boostrap.py
+ bin/buildout
+ bin/nosetests
Todo
====
- - Substitution and other unit testing
- Support storing configuration options locally (eg repo options that don't
get sent to GitHub)
- Send emails to owners about removing repos
View
121 bootstrap.py
@@ -0,0 +1,121 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+# parsing arguments
+parser = OptionParser()
+parser.add_option("-v", "--version", dest="version",
+ help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+ action="store_true", dest="distribute", default=False,
+ help="Use Disribute rather than Setuptools.")
+
+parser.add_option("-c", None, action="store", dest="config_file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+ args += ['-c', options.config_file]
+
+if options.version is not None:
+ VERSION = '==%s' % options.version
+else:
+ VERSION = ''
+
+USE_DISTRIBUTE = options.distribute
+args = args + ['bootstrap']
+
+to_reload = False
+try:
+ import pkg_resources
+ if not hasattr(pkg_resources, '_distribute'):
+ to_reload = True
+ raise ImportError
+except ImportError:
+ ez = {}
+ if USE_DISTRIBUTE:
+ exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True)
+ else:
+ exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+ ).read() in ez
+ ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+ if to_reload:
+ reload(pkg_resources)
+ else:
+ import pkg_resources
+
+if sys.platform == 'win32':
+ def quote(c):
+ if ' ' in c:
+ return '"%s"' % c # work around spawn lamosity on windows
+ else:
+ return c
+else:
+ def quote (c):
+ return c
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+ws = pkg_resources.working_set
+
+if USE_DISTRIBUTE:
+ requirement = 'distribute'
+else:
+ requirement = 'setuptools'
+
+if is_jython:
+ import subprocess
+
+ assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
+ quote(tmpeggs), 'zc.buildout' + VERSION],
+ env=dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse(requirement)).location
+ ),
+ ).wait() == 0
+
+else:
+ assert os.spawnle(
+ os.P_WAIT, sys.executable, quote (sys.executable),
+ '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION,
+ dict(os.environ,
+ PYTHONPATH=
+ ws.find(pkg_resources.Requirement.parse(requirement)).location
+ ),
+ ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout' + VERSION)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
View
11 buildout.cfg
@@ -0,0 +1,11 @@
+[buildout]
+parts = test
+develop = .
+package-name = github-collective
+
+[test]
+recipe = zc.recipe.egg
+eggs =
+ ${buildout:package-name} [test]
+scripts = ${buildout:package-name} nosetests
+dependent-scripts = true
View
41 githubcollective/config.py
@@ -4,7 +4,6 @@
except:
import json
-import itertools
import os
import re
import requests
@@ -115,16 +114,16 @@ def get_fork_url(self, repo):
_template_split = re.compile('([$]{[^}]*?})').split
-def _do_sub(value, config, current_section=None, stack=(), local=False):
+def substitute(value, config, context=None, local=False, stack=()):
"""Carry out subsitution of values in the form ${section:option}.
`value`: the original value to have substitution applied.
`config`: an instance of a ConfigParser.
- `current_section`: the identifier of the current context (used to
+ `context`: the identifier of the current section context (used to
determine empty section name lookups).
`stack`: the current tuple of fields inspected through recursion.
`local`: resolve local references (eg Hook references Repo) against
- the given ``current_section``.
+ the given ``context``.
Strongly influenced by what ``zc.buildout`` does.
"""
@@ -138,7 +137,7 @@ def _do_sub(value, config, current_section=None, stack=(), local=False):
#Support ${:option} and ${repo:option} syntaxes
if not section or local:
- section = current_section
+ section = context
#Only lookup now if substituting from global config
if section not in LOCAL_SECTION_PREFIXES:
@@ -150,7 +149,7 @@ def _do_sub(value, config, current_section=None, stack=(), local=False):
raise ValueError(
"Circular reference in substitutions."
)
- sub = _do_sub(sub, config, section, stack + (ref,))
+ sub = substitute(sub, config, section, stack + (ref,))
subs.append(sub)
#Leave alone until we process the context
else:
@@ -161,6 +160,22 @@ def _do_sub(value, config, current_section=None, stack=(), local=False):
substitution = ''.join([''.join(v) for v in zip(parts[::2], subs)])
return substitution
+def global_substitute(config):
+ """Take care of all global configuration substitutions.
+
+ This function will not adjust `local` substitions as these
+ cannot be resolved until a relevant context is available later.
+ `config` will be modified in place.
+ """
+ for section in config.sections():
+ for option, value in config.items(section):
+ if '${' in value:
+ sub_value = substitute(value=value,
+ config=config,
+ context=section,
+ local=False)
+ config.set(section, option, sub_value)
+
class ConfigCFG(Config):
@@ -171,11 +186,7 @@ def parse(self, data):
config.readfp(StringIO.StringIO(data))
# global substitutions in ${section:option} style
- for section in config.sections():
- for option, value in config.items(section):
- if '${' in value:
- sub_value = _do_sub(value, config, section)
- config.set(section, option, sub_value)
+ global_substitute(config)
for section in config.sections():
if section.startswith('repo:'):
@@ -201,10 +212,10 @@ def parse(self, data):
for config_key, config_value in config.items(hook_section):
# local variable substitution
if '${' in config_value:
- config_value = _do_sub(config_value,
- config,
- section,
- local=True)
+ config_value = substitute(value=config_value,
+ config=config,
+ context=section,
+ local=True)
config.set(hook_section, config_key, config_value)
# coerce values into correct formats
View
5 setup.cfg
@@ -0,0 +1,5 @@
+[nosetests]
+with-doctest=1
+doctest-extension=rst
+tests=README.rst,githubcollective/
+
View
3 setup.py
@@ -30,6 +30,9 @@
'argparse',
'requests==0.13.1',
],
+ extras_require={
+ 'test': ['nose']
+ },
entry_points="""
[console_scripts]
github-collective = githubcollective:run

0 comments on commit e215964

Please sign in to comment.