Repository: fedora-infra/bodhi

Issue:
Cannot run bodhi/tests/server/services/test_updates.py in isolation
Running ```bodhi/tests/server/services/test_updates.py``` in isolation seems to run into a circular import problem:

```
$ time nosetests -x bodhi/tests/server/services/test_updates.py                            
E
======================================================================
ERROR: test_cascade_package_requirements_to_update (bodhi.tests.server.services.test_updates.TestNewUpdate)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/vagrant/bodhi/tests/server/functional/base.py", line 79, in setUp
    self.app = TestApp(main({}, testing=u'guest', session=self.db, **self.app_settings))
  File "/vagrant/bodhi/server/__init__.py", line 225, in main
    config.scan('bodhi.server.services')
  File "/usr/lib/python2.7/site-packages/pyramid/config/__init__.py", line 930, in scan
    ignore=ignore)
  File "/usr/lib/python2.7/site-packages/venusian/__init__.py", line 197, in scan
    __import__(modname)
  File "/vagrant/bodhi/server/services/builds.py", line 22, in <module>
    from bodhi.server.validators import validate_updates, validate_packages, validate_releases
  File "/vagrant/bodhi/server/validators.py", line 32, in <module>
    import bodhi.server.schemas
  File "/vagrant/bodhi/server/schemas.py", line 24, in <module>
    from bodhi.server.validators import validate_csrf_token
ImportError: cannot import name validate_csrf_token
```


Discussion:
I have no idea how this problem is avoided when the tests are all run, but it does seem that ```validators``` is importing ```schemas``` and vice-versa, which is usually a recipe for trouble and often implies that they might should be the same module.
It seems the ```validators``` only imports schemas for a single function. It may be possible to move that function into a third module, such as the utils module.

Resolving patch:
diff --git a/bodhi/server/schemas.py b/bodhi/server/schemas.py
--- a/bodhi/server/schemas.py
+++ b/bodhi/server/schemas.py
@@ -16,34 +16,15 @@
 
 import colander
 
-from kitchen.iterutils import iterate
-
+from bodhi.server import util
 from bodhi.server.models import (UpdateRequest, UpdateSeverity, UpdateStatus,
                           UpdateSuggestion, UpdateType, ReleaseState)
-
 from bodhi.server.validators import validate_csrf_token
 
 
 CVE_REGEX = re.compile(r"CVE-[0-9]{4,4}-[0-9]{4,}")
 
 
-def splitter(value):
-    """Parse a string or list of comma or space delimited builds"""
-    if value == colander.null:
-        return
-
-    items = []
-    for v in iterate(value):
-        if isinstance(v, basestring):
-            for item in v.replace(',', ' ').split():
-                items.append(item)
-
-        elif v is not None:
-            items.append(v)
-
-    return items
-
-
 class CSRFProtectedSchema(colander.MappingSchema):
     csrf_token = colander.SchemaNode(
         colander.String(),
@@ -145,12 +126,12 @@ class SaveCommentSchema(CSRFProtectedSchema, colander.MappingSchema):
 
 class SaveUpdateSchema(CSRFProtectedSchema, colander.MappingSchema):
     builds = Builds(colander.Sequence(accept_scalar=True),
-                    preparer=[splitter])
+                    preparer=[util.splitter])
 
     bugs = Bugs(
             colander.Sequence(accept_scalar=True),
             missing=None,
-            preparer=[splitter]
+            preparer=[util.splitter]
     )
 
     close_bugs = colander.SchemaNode(
@@ -261,14 +242,14 @@ class ListReleaseSchema(PaginatedSchema):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     packages = Packages(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
 
@@ -338,7 +319,7 @@ class ListStackSchema(PaginatedSchema, SearchableSchema):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
 
@@ -350,7 +331,7 @@ class SaveStackSchema(CSRFProtectedSchema, colander.MappingSchema):
     packages = Packages(
         colander.Sequence(accept_scalar=True),
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     description = colander.SchemaNode(
@@ -375,21 +356,21 @@ class ListUserSchema(PaginatedSchema, SearchableSchema):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     updates = Updates(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     packages = Packages(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
 
@@ -398,7 +379,7 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     approved_since = colander.SchemaNode(
@@ -417,14 +398,14 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     builds = Builds(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     critpath = colander.SchemaNode(
@@ -437,7 +418,7 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     locked = colander.SchemaNode(
@@ -462,7 +443,7 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     pushed = colander.SchemaNode(
@@ -487,7 +468,7 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     # This singular version of the plural "releases" is purely for bodhi1
@@ -555,7 +536,7 @@ class ListUpdateSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
 
@@ -578,21 +559,21 @@ class ListBuildSchema(PaginatedSchema):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     releases = Releases(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     updates = Updates(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
 
@@ -608,14 +589,14 @@ class ListCommentSchema(PaginatedSchema, SearchableSchema):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     packages = Packages(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     user = colander.SchemaNode(
@@ -660,14 +641,14 @@ class ListOverrideSchema(PaginatedSchema, SearchableSchema, Cosmetics):
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     releases = Releases(
         colander.Sequence(accept_scalar=True),
         location="querystring",
         missing=None,
-        preparer=[splitter],
+        preparer=[util.splitter],
     )
 
     user = colander.SchemaNode(
diff --git a/bodhi/server/util.py b/bodhi/server/util.py
--- a/bodhi/server/util.py
+++ b/bodhi/server/util.py
@@ -30,11 +30,13 @@
 import tempfile
 import urllib
 
+from kitchen.iterutils import iterate
 from pyramid.i18n import TranslationStringFactory
 from pyramid.settings import asbool
 from sqlalchemy.orm import scoped_session, sessionmaker
 from zope.sqlalchemy import ZopeTransactionExtension
 import arrow
+import colander
 import libravatar
 import markdown
 import requests
@@ -296,6 +298,23 @@ def work(username, size):
     return work(username, size)
 
 
+def splitter(value):
+    """Parse a string or list of comma or space delimited builds"""
+    if value == colander.null:
+        return
+
+    items = []
+    for v in iterate(value):
+        if isinstance(v, basestring):
+            for item in v.replace(',', ' ').split():
+                items.append(item)
+
+        elif v is not None:
+            items.append(v)
+
+    return items
+
+
 def version(context=None):
     return pkg_resources.get_distribution('bodhi').version
 
diff --git a/bodhi/server/validators.py b/bodhi/server/validators.py
--- a/bodhi/server/validators.py
+++ b/bodhi/server/validators.py
@@ -28,8 +28,7 @@
                      UpdateRequest, UpdateSeverity, UpdateType,
                      UpdateSuggestion, User, Group, Comment,
                      Bug, TestCase, ReleaseState, Stack)
-from .util import get_nvr, tokenize, taskotron_results
-import bodhi.server.schemas
+from .util import get_nvr, splitter, tokenize, taskotron_results
 
 
 csrf_error_message = """CSRF tokens do not match.  This happens if you have
@@ -745,7 +744,7 @@ def validate_comment_id(request):
 
 def validate_override_builds(request):
     """ Ensure that the build is properly tagged """
-    nvrs = bodhi.server.schemas.splitter(request.validated['nvr'])
+    nvrs = splitter(request.validated['nvr'])
     db = request.db
 
     if not nvrs:


Test patch: