Volans has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/363750 )

Change subject: QueryBuilder: move query string to build() method
......................................................................

QueryBuilder: move query string to build() method

* Breaking change: the constructor of the QueryBuilder was changed to
  not accept anymore a query string directly, but just the configuration
  and the optional logger. The query string is now a required parameter
  of the build() method.
  This properly split configuration and parameters, allowing to easily
  build() multiple queries with the same QueryBuilder instance.

Change-Id: I27e4576d44b0772ab9b843b5816a932fb352a962
---
M cumin/cli.py
M cumin/query.py
M cumin/tests/unit/test_query.py
3 files changed, 63 insertions(+), 77 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/operations/software/cumin 
refs/changes/50/363750/1

diff --git a/cumin/cli.py b/cumin/cli.py
index 5bc038b..a318cbe 100644
--- a/cumin/cli.py
+++ b/cumin/cli.py
@@ -231,7 +231,7 @@
     args   -- ArgumentParser instance with parsed command line arguments
     config -- a dictionary with the parsed configuration file
     """
-    query = QueryBuilder(args.hosts, config, logger).build()
+    query = QueryBuilder(config, logger).build(args.hosts)
     hosts = query.execute()
 
     if not hosts:
diff --git a/cumin/query.py b/cumin/query.py
index bf2ff59..26a5e1d 100644
--- a/cumin/query.py
+++ b/cumin/query.py
@@ -35,23 +35,26 @@
     Parse a given query string and converts it into a query object for the 
configured backend
     """
 
-    def __init__(self, query_string, config, logger=None):
+    def __init__(self, config, logger=None):
         """Query builder constructor.
 
         Arguments:
-        query_string -- the query string to be parsed and passed to the query 
builder
         config       -- the configuration dictionary
         logger       -- an optional logging instance [optional, default: None]
         """
         self.logger = logger or logging.getLogger(__name__)
-        self.query_string = query_string.strip()
         self.query = Query.new(config, logger=self.logger)
         self.aliases = config.get(config['backend'], {}).get('aliases', {})
-        self.level = 0  # Nesting level for sub-groups
+        self.level = None  # Nesting level for sub-groups
 
-    def build(self):
-        """Parse the query string according to the grammar and build the query 
object for the configured backend."""
-        parsed = grammar.parseString(self.query_string, parseAll=True)
+    def build(self, query_string):
+        """Parse the query string according to the grammar and build the query 
object for the configured backend.
+
+        Arguments:
+        query_string -- the query string to be parsed and passed to the query 
builder
+        """
+        self.level = 0
+        parsed = grammar.parseString(query_string.strip(), parseAll=True)
         for token in parsed:
             self._parse_token(token)
 
@@ -64,9 +67,9 @@
         token -- a single token returned by the grammar parsing
         level -- nesting level in case of sub-groups in the query [optional, 
default: 0]
         """
-        if not isinstance(token, ParseResults):
-            raise InvalidQueryError("Invalid query string syntax '{query}'. 
Token is '{token}'".format(
-                query=self.query_string, token=token))
+        if not isinstance(token, ParseResults):  # Non-testable block, this 
should never happen
+            raise InvalidQueryError('Expected an instance of 
pyparsing.ParseResults, got {instance}: {token}'.format(
+                instance=type(token), token=token))
 
         token_dict = token.asDict()
         if not token_dict:
@@ -103,6 +106,8 @@
                 self.query.add_and()
             elif token_dict['bool'] == 'or':
                 self.query.add_or()
+            else:  # Non-testable block, this should never happen with the 
current grammar
+                raise InvalidQueryError("Got bool '{bool}', one of and|or 
expected".format(bool=token_dict['bool']))
 
         elif 'hosts' in keys:
             token_dict['hosts'] = NodeSet(token_dict['hosts'])
@@ -111,6 +116,10 @@
         elif 'category' in keys:
             self.query.add_category(**token_dict)
 
+        else:  # Non-testable block, this should never happen with the current 
grammar
+            raise InvalidQueryError(
+                "No valid key found in token, one of bool|hosts|category 
expected: {token}".format(token=token_dict))
+
     def _replace_alias(self, token_dict, level):
         """Replace any alias in the query in a recursive way, alias can 
reference other aliases.
 
diff --git a/cumin/tests/unit/test_query.py b/cumin/tests/unit/test_query.py
index 7ea6a08..1a2d67e 100644
--- a/cumin/tests/unit/test_query.py
+++ b/cumin/tests/unit/test_query.py
@@ -69,115 +69,92 @@
     }
 
     @mock.patch('cumin.query.Query', QueryFactory)
+    def setup_method(self, _):
+        """Set method for each test, init a QueryBuilder."""
+        self.query_builder = QueryBuilder(self.config)  # pylint: 
disable=attribute-defined-outside-init
+
     def test_instantiation(self):
         """Class QueryBuilder should create an instance of a query_class for 
the given backend."""
-        query_builder = QueryBuilder(self.query_string, self.config)
-        assert isinstance(query_builder, QueryBuilder)
-        assert isinstance(query_builder.query, BaseQuery)
-        assert query_builder.query_string == self.query_string
-        assert query_builder.level == 0
+        assert isinstance(self.query_builder, QueryBuilder)
+        assert isinstance(self.query_builder.query, BaseQuery)
+        assert self.query_builder.level is None
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_valid(self):
         """QueryBuilder.build() should parse and build the query object for a 
valid query."""
-        query_builder = QueryBuilder(self.query_string, self.config)
-        query_builder.build()
+        self.query_builder.build(self.query_string)
 
-        query_builder.query.add_hosts.assert_has_calls(
+        self.query_builder.query.add_hosts.assert_has_calls(
             [mock.call(hosts=NodeSet('host1')), 
mock.call(hosts=NodeSet('host2'))])
-        query_builder.query.add_or.assert_has_calls([mock.call(), mock.call()])
-        query_builder.query.open_subgroup.assert_called_once_with()
-        query_builder.query.add_category.assert_has_calls([
+        self.query_builder.query.add_or.assert_has_calls([mock.call(), 
mock.call()])
+        self.query_builder.query.open_subgroup.assert_called_once_with()
+        self.query_builder.query.add_category.assert_has_calls([
             mock.call(category='F', key='key1', operator='=', value='value', 
neg='not'),
             mock.call(category='R', key='key2', operator='~', value='regex')])
-        query_builder.query.add_and.assert_called_once_with()
-        query_builder.query.close_subgroup.assert_called_once_with()
-        assert query_builder.level == 0
+        self.query_builder.query.add_and.assert_called_once_with()
+        self.query_builder.query.close_subgroup.assert_called_once_with()
+        assert self.query_builder.level == 0
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_valid_with_aliases(self):
         """QueryBuilder.build() should replace any aliases and build the query 
object for a valid query."""
-        query_builder = QueryBuilder('host100 or A:group1 or host2', 
self.config)
-        query_builder.build()
+        self.query_builder.build('host100 or A:group1 or host2')
 
-        query_builder.query.add_hosts.assert_has_calls(
+        self.query_builder.query.add_hosts.assert_has_calls(
             [mock.call(hosts=NodeSet('host100')), 
mock.call(hosts=NodeSet('host1')),
              mock.call(hosts=NodeSet('host10[10-22]')), 
mock.call(hosts=NodeSet('host2'))])
-        query_builder.query.add_or.assert_has_calls([mock.call(), mock.call(), 
mock.call()])
-        query_builder.query.open_subgroup.assert_called_once_with()
-        query_builder.query.close_subgroup.assert_called_once_with()
-        assert query_builder.level == 0
+        self.query_builder.query.add_or.assert_has_calls([mock.call(), 
mock.call(), mock.call()])
+        self.query_builder.query.open_subgroup.assert_called_once_with()
+        self.query_builder.query.close_subgroup.assert_called_once_with()
+        assert self.query_builder.level == 0
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_valid_with_nested_aliases(self):  # pylint: 
disable=invalid-name
         """QueryBuilder.build() should replace any aliases and build the query 
object for a valid query."""
-        query_builder = QueryBuilder('host100 or A:nested_group', self.config)
-        query_builder.build()
+        self.query_builder.build('host100 or A:nested_group')
 
-        query_builder.query.add_hosts.assert_has_calls(
+        self.query_builder.query.add_hosts.assert_has_calls(
             [mock.call(hosts=NodeSet('host100')), 
mock.call(hosts=NodeSet('host10[40-42]')),
              mock.call(hosts=NodeSet('host1')), 
mock.call(hosts=NodeSet('host10[10-22]'))])
-        query_builder.query.add_or.assert_has_calls([mock.call(), mock.call()])
-        query_builder.query.open_subgroup.assert_has_calls([mock.call(), 
mock.call()])
-        query_builder.query.close_subgroup.assert_has_calls([mock.call(), 
mock.call()])
-        assert query_builder.level == 0
+        self.query_builder.query.add_or.assert_has_calls([mock.call(), 
mock.call()])
+        self.query_builder.query.open_subgroup.assert_has_calls([mock.call(), 
mock.call()])
+        self.query_builder.query.close_subgroup.assert_has_calls([mock.call(), 
mock.call()])
+        assert self.query_builder.level == 0
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_invalid_alias_syntax(self):
         """QueryBuilder.build() should raise InvalidQueryError if an alias has 
invalid syntax."""
-        query_builder = QueryBuilder('host1 or A:name = value', self.config)
         with pytest.raises(InvalidQueryError, match='Invalid alias syntax, 
aliases can be only of the form'):
-            query_builder.build()
+            self.query_builder.build('host1 or A:name = value')
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_missing_alias(self):
         """QueryBuilder.build() should raise InvalidQueryError if a non 
existent alias is found."""
-        query_builder = QueryBuilder('host1 or A:non_existent_group', 
self.config)
         with pytest.raises(InvalidQueryError, match='Unable to find alias 
replacement for'):
-            query_builder.build()
+            self.query_builder.build('host1 or A:non_existent_group')
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_glob_host(self):
         """QueryBuilder.build() should parse a glob host."""
-        query_builder = QueryBuilder('host1*', self.config)
-        query_builder.build()
-        
query_builder.query.add_hosts.assert_called_once_with(hosts=NodeSet('host1*'))
-        assert query_builder.level == 0
+        self.query_builder.build('host1*')
+        
self.query_builder.query.add_hosts.assert_called_once_with(hosts=NodeSet('host1*'))
+        assert self.query_builder.level == 0
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_invalid(self):
         """QueryBuilder.build() should raise ParseException for an invalid 
query."""
-        query_builder = QueryBuilder(self.invalid_query_string, self.config)
         with pytest.raises(ParseException, match='Expected end of text'):
-            query_builder.build()
+            self.query_builder.build(self.invalid_query_string)
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_subgroup(self):
         """QueryBuilder.build() should open and close a subgroup properly."""
-        query_builder = QueryBuilder('(host1)', self.config)
-        query_builder.build()
+        self.query_builder.build('(host1)')
 
-        
query_builder.query.add_hosts.assert_has_calls([mock.call(hosts=NodeSet('host1'))])
-        query_builder.query.open_subgroup.assert_called_once_with()
-        query_builder.query.close_subgroup.assert_called_once_with()
-        assert query_builder.level == 0
+        
self.query_builder.query.add_hosts.assert_has_calls([mock.call(hosts=NodeSet('host1'))])
+        self.query_builder.query.open_subgroup.assert_called_once_with()
+        self.query_builder.query.close_subgroup.assert_called_once_with()
+        assert self.query_builder.level == 0
 
-    @mock.patch('cumin.query.Query', QueryFactory)
     def test_build_subgroups(self):
         """QueryBuilder.build() should open and close multiple subgroups 
properly."""
-        query_builder = QueryBuilder('(host1 or (host2 or host3))', 
self.config)
-        query_builder.build()
+        self.query_builder.build('(host1 or (host2 or host3))')
 
-        query_builder.query.add_hosts.assert_has_calls(
+        self.query_builder.query.add_hosts.assert_has_calls(
             [mock.call(hosts=NodeSet('host1')), 
mock.call(hosts=NodeSet('host2')),
              mock.call(hosts=NodeSet('host3'))])
-        query_builder.query.open_subgroup.assert_has_calls([mock.call(), 
mock.call()])
-        query_builder.query.close_subgroup.assert_has_calls([mock.call(), 
mock.call()])
-        assert query_builder.level == 0
-
-    @mock.patch('cumin.query.Query', QueryFactory)
-    def test__parse_token(self):
-        """QueryBuilder._parse_token() should raise RuntimeError for an 
invalid token."""
-        query_builder = QueryBuilder(self.invalid_query_string, self.config)
-        with pytest.raises(InvalidQueryError, match='Invalid query string 
syntax'):
-            query_builder._parse_token('invalid_token')  # pylint: 
disable=protected-access
+        self.query_builder.query.open_subgroup.assert_has_calls([mock.call(), 
mock.call()])
+        self.query_builder.query.close_subgroup.assert_has_calls([mock.call(), 
mock.call()])
+        assert self.query_builder.level == 0

-- 
To view, visit https://gerrit.wikimedia.org/r/363750
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I27e4576d44b0772ab9b843b5816a932fb352a962
Gerrit-PatchSet: 1
Gerrit-Project: operations/software/cumin
Gerrit-Branch: master
Gerrit-Owner: Volans <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to