Repository: lark-parser/lark
Pull request #607 (2020-06-30 23:21:18)

Avoid using root logger
#604 


Patch:
diff --git a/docs/how_to_use.md b/docs/how_to_use.md
index 886b440..303098f 100644
--- a/docs/how_to_use.md
+++ b/docs/how_to_use.md
@@ -30,12 +30,13 @@ Use the reference pages for more in-depth explanations. (links in the [main page
 
 ## LALR usage
 
-By default Lark silently resolves Shift/Reduce conflicts as Shift. To enable warnings pass `debug=True`. To get the messages printed you have to configure `logging` framework beforehand. For example:
+By default Lark silently resolves Shift/Reduce conflicts as Shift. To enable warnings pass `debug=True`. To get the messages printed you have to configure the `logger` beforehand. For example:
 
 ```python
-from lark import Lark
 import logging
-logging.basicConfig(level=logging.DEBUG)
+from lark import Lark, logger
+
+logger.setLevel(logging.DEBUG)
 
 collision_grammar = '''
 start: as as
diff --git a/lark/__init__.py b/lark/__init__.py
index 9e50691..e3021cf 100644
--- a/lark/__init__.py
+++ b/lark/__init__.py
@@ -1,3 +1,4 @@
+from .common import logger
 from .tree import Tree
 from .visitors import Transformer, Visitor, v_args, Discard
 from .visitors import InlineTransformer, inline_args   # XXX Deprecated
diff --git a/lark/common.py b/lark/common.py
index c44f9ce..745e287 100644
--- a/lark/common.py
+++ b/lark/common.py
@@ -1,6 +1,13 @@
+import logging
 from .utils import Serialize
 from .lexer import TerminalDef
 
+logger = logging.getLogger("lark")
+logger.addHandler(logging.StreamHandler())
+# Set to highest level, since we have some warnings amongst the code
+# By default, we should not output any log messages
+logger.setLevel(logging.CRITICAL)
+
 ###{standalone
 
 class LexerConf(Serialize):
diff --git a/lark/lark.py b/lark/lark.py
index 2b783cb..9bb60c8 100644
--- a/lark/lark.py
+++ b/lark/lark.py
@@ -1,13 +1,13 @@
 from __future__ import absolute_import
 
-import sys, os, pickle, hashlib, logging
+import sys, os, pickle, hashlib
 from io import open
 
 
 from .utils import STRING_TYPE, Serialize, SerializeMemoizer, FS
 from .load_grammar import load_grammar
 from .tree import Tree
-from .common import LexerConf, ParserConf
+from .common import LexerConf, ParserConf, logger
 
 from .lexer import Lexer, TraditionalLexer, TerminalDef, UnexpectedToken
 from .parse_tree_builder import ParseTreeBuilder
@@ -205,7 +205,7 @@ class Lark(Serialize):
                 cache_fn = '.lark_cache_%s.tmp' % md5
 
             if FS.exists(cache_fn):
-                logging.debug('Loading grammar from cache: %s', cache_fn)
+                logger.debug('Loading grammar from cache: %s', cache_fn)
                 with FS.open(cache_fn, 'rb') as f:
                     self._load(f, self.options.transformer, self.options.postlex)
                 return
@@ -284,7 +284,7 @@ class Lark(Serialize):
             self.lexer = self._build_lexer()
 
         if cache_fn:
-            logging.debug('Saving grammar to cache: %s', cache_fn)
+            logger.debug('Saving grammar to cache: %s', cache_fn)
             with FS.open(cache_fn, 'wb') as f:
                 self.save(f)
 
diff --git a/lark/parsers/earley.py b/lark/parsers/earley.py
index 59e9a06..bf099e6 100644
--- a/lark/parsers/earley.py
+++ b/lark/parsers/earley.py
@@ -10,11 +10,11 @@ is better documented here:
     http://www.bramvandersanden.com/post/2014/06/shared-packed-parse-forest/
 """
 
-import logging
 from collections import deque
 
 from ..visitors import Transformer_InPlace, v_args
 from ..exceptions import UnexpectedEOF, UnexpectedToken
+from ..common import logger
 from .grammar_analysis import GrammarAnalyzer
 from ..grammar import NonTerminal
 from .earley_common import Item, TransitiveItem
@@ -301,7 +301,7 @@ class Parser:
             try:
                 debug_walker = ForestToPyDotVisitor()
             except ImportError:
-                logging.warning("Cannot find dependency 'pydot', will not generate sppf debug image")
+                logger.warning("Cannot find dependency 'pydot', will not generate sppf debug image")
             else:
                 debug_walker.visit(solutions[0], "sppf.png")
 
diff --git a/lark/parsers/lalr_analysis.py b/lark/parsers/lalr_analysis.py
index 8890c3c..861941f 100644
--- a/lark/parsers/lalr_analysis.py
+++ b/lark/parsers/lalr_analysis.py
@@ -6,11 +6,11 @@ For now, shift/reduce conflicts are automatically resolved as shifts.
 # Author: Erez Shinan (2017)
 # Email : erezshin@gmail.com
 
-import logging
 from collections import defaultdict, deque
 
 from ..utils import classify, classify_bool, bfs, fzset, Serialize, Enumerator
 from ..exceptions import GrammarError
+from ..common import logger
 
 from .grammar_analysis import GrammarAnalyzer, Terminal, LR0ItemSet
 from ..grammar import Rule
@@ -256,8 +256,8 @@ class LALR_Analyzer(GrammarAnalyzer):
                     raise GrammarError('Reduce/Reduce collision in %s between the following rules: %s' % (la, ''.join([ '\n\t\t- ' + str(r) for r in rules ])))
                 if la in actions:
                     if self.debug:
-                        logging.warning('Shift/Reduce conflict for terminal %s: (resolving as shift)', la.name)
-                        logging.warning(' * %s', list(rules)[0])
+                        logger.warning('Shift/Reduce conflict for terminal %s: (resolving as shift)', la.name)
+                        logger.warning(' * %s', list(rules)[0])
                 else:
                     actions[la] = (Reduce, list(rules)[0])
             m[state] = { k.name: v for k, v in actions.items() }


Test patch:
diff --git a/tests/__main__.py b/tests/__main__.py
index cb26eb4..9ef9f1b 100644
--- a/tests/__main__.py
+++ b/tests/__main__.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import, print_function
 
 import unittest
 import logging
+from lark import logger
 
 from .test_trees import TestTrees
 from .test_tools import TestStandalone
@@ -11,11 +12,13 @@ from .test_reconstructor import TestReconstructor
 try:
     from .test_nearley.test_nearley import TestNearley
 except ImportError:
-    logging.warning("Warning: Skipping tests for Nearley grammar imports (js2py required)")
+    logger.warning("Warning: Skipping tests for Nearley grammar imports (js2py required)")
 
 # from .test_selectors import TestSelectors
 # from .test_grammars import TestPythonG, TestConfigG
 
+from .test_logger import Testlogger
+
 from .test_parser import (
         TestLalrStandard,
         TestEarleyStandard,
@@ -31,7 +34,7 @@ from .test_parser import (
         TestParsers,
         )
 
-logging.basicConfig(level=logging.INFO)
+logger.setLevel(logging.INFO)
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_logger.py b/tests/test_logger.py
new file mode 100644
index 0000000..93dc8ed
--- /dev/null
+++ b/tests/test_logger.py
@@ -0,0 +1,65 @@
+import logging
+from contextlib import contextmanager
+from lark import Lark, logger
+from unittest import TestCase, main
+
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
+
+@contextmanager
+def capture_log():
+    stream = StringIO()
+    orig_handler = logger.handlers[0]
+    del logger.handlers[:]
+    logger.addHandler(logging.StreamHandler(stream))
+    yield stream
+    del logger.handlers[:]
+    logger.addHandler(orig_handler)
+
+class Testlogger(TestCase):
+
+    def test_debug(self):
+        logger.setLevel(logging.DEBUG)
+        collision_grammar = '''
+        start: as as
+        as: a*
+        a: "a"
+        '''
+        with capture_log() as log:
+            Lark(collision_grammar, parser='lalr', debug=True)
+
+        log = log.getvalue()
+        # since there are conflicts about A
+        # symbol A should appear in the log message for hint
+        self.assertIn("A", log)
+
+    def test_non_debug(self):
+        logger.setLevel(logging.DEBUG)
+        collision_grammar = '''
+        start: as as
+        as: a*
+        a: "a"
+        '''
+        with capture_log() as log:
+            Lark(collision_grammar, parser='lalr', debug=False)
+        log = log.getvalue()
+        # no log messge
+        self.assertEqual(len(log), 0)
+
+    def test_loglevel_higher(self):
+        logger.setLevel(logging.ERROR)
+        collision_grammar = '''
+        start: as as
+        as: a*
+        a: "a"
+        '''
+        with capture_log() as log:
+            Lark(collision_grammar, parser='lalr', debug=True)
+        log = log.getvalue()
+        # no log messge
+        self.assertEqual(len(log), 0)
+
+if __name__ == '__main__':
+    main()
diff --git a/tests/test_nearley/test_nearley.py b/tests/test_nearley/test_nearley.py
index 647f489..1ad6449 100644
--- a/tests/test_nearley/test_nearley.py
+++ b/tests/test_nearley/test_nearley.py
@@ -6,16 +6,17 @@ import logging
 import os
 import codecs
 
-logging.basicConfig(level=logging.INFO)
-
+from lark import logger
 from lark.tools.nearley import create_code_for_nearley_grammar, main as nearley_tool_main
 
+logger.setLevel(logging.INFO)
+
 TEST_PATH    = os.path.abspath(os.path.dirname(__file__))
 NEARLEY_PATH = os.path.join(TEST_PATH, 'nearley')
 BUILTIN_PATH = os.path.join(NEARLEY_PATH, 'builtin')
 
 if not os.path.exists(NEARLEY_PATH):
-    logging.warn("Nearley not installed. Skipping Nearley tests!")
+    logger.warn("Nearley not installed. Skipping Nearley tests!")
     raise ImportError("Skipping Nearley tests!")
 
 import js2py    # Ensures that js2py exists, to avoid failing tests
diff --git a/tests/test_parser.py b/tests/test_parser.py
index df09307..88d175f 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -18,13 +18,13 @@ from io import (
         open,
     )
 
-logging.basicConfig(level=logging.INFO)
 
 try:
     import regex
 except ImportError:
     regex = None
 
+from lark import logger
 from lark.lark import Lark
 from lark.exceptions import GrammarError, ParseError, UnexpectedToken, UnexpectedInput, UnexpectedCharacters
 from lark.tree import Tree
@@ -32,6 +32,7 @@ from lark.visitors import Transformer, Transformer_InPlace, v_args
 from lark.grammar import Rule
 from lark.lexer import TerminalDef, Lexer, TraditionalLexer
 
+logger.setLevel(logging.INFO)
 
 
 __path__ = os.path.dirname(__file__)