Repository: google/yapf
Pull request #108 (2015-04-17 01:12:24)

Add support for project-local style (in .style.yapf)
YAPF will now look for this file at startup and use it as
the default if found.  If not found pep8 is still the default.

Now that we have project-local style there is no need for the
hardcoded yapf style (it will be the default for all files
in the yapf tree now).

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2357939/badge)](https://coveralls.io/builds/2357939)

Coverage decreased (-0.2%) to 91.51% when pulling **6679c5f23177086f72d1398a46c2f4108598e8f2 on sbc100:local_config** into **5a6fa8128f63b8b0b4cfb95120bcd62ce2834f8f on google:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/2357939/badge)](https://coveralls.io/builds/2357939)

Coverage decreased (-0.2%) to 91.51% when pulling **6679c5f23177086f72d1398a46c2f4108598e8f2 on sbc100:local_config** into **5a6fa8128f63b8b0b4cfb95120bcd62ce2834f8f on google:master**.

gwelymernans: What's the status of this pull request? @eliben, do you have any issues here? I have some preference, but this area is not my specialty. So I am comfortable to defer to someone else on this. :-)
eliben: I think both you and I left review comments for @sbc100 and are waiting for them to be addressed?
sbc100: Yes, I have some work to do still here.  Will try to get it updated today.
sbc100: PTAL

Patch:
diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..823a973
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,3 @@
+[style]
+# YAPF uses the chromium style
+based_on_style = chromium
diff --git a/README.rst b/README.rst
index 66517ce..8210cac 100644
--- a/README.rst
+++ b/README.rst
@@ -74,7 +74,9 @@ Usage
 
 Options::
 
-    usage: yapf [-h] [--style STYLE] [-d | -i] [-l START-END | -r] ...
+    usage: yapf [-h] [--version] [--style-help] [--style STYLE] [--verify]
+                     [-d | -i] [-l START-END | -r]
+                     [files [files ...]]
 
     Formatter for Python code.
 
@@ -83,9 +85,15 @@ Options::
 
     optional arguments:
       -h, --help            show this help message and exit
+      --version             show version number and exit
+      --style-help          show style settings and exit
       --style STYLE         specify formatting style: either a style name (for
                             example "pep8" or "google"), or the name of a file
-                            with style settings. pep8 is the default.
+                            with style settings. The default is pep8 unless a
+                            .style.yapf file located in one of the parent
+                            directories of the source file (or current directory
+                            for stdin)
+      --verify              try to verify refomatted code for syntax errors
       -d, --diff            print the diff for the fixed source
       -i, --in-place        make changes to files in place
       -l START-END, --lines START-END
diff --git a/yapf/__init__.py b/yapf/__init__.py
index b75c29c..7634c5c 100644
--- a/yapf/__init__.py
+++ b/yapf/__init__.py
@@ -29,6 +29,7 @@ from __future__ import print_function
 
 import argparse
 import logging
+import os
 import sys
 
 from yapf.yapflib import file_resources
@@ -66,10 +67,15 @@ def main(argv):
   parser.add_argument(
       '--style',
       action='store',
-      default='pep8',
       help=('specify formatting style: either a style name (for example "pep8" '
-            'or "google"), or the name of a file with style settings. pep8 is '
-            'the default.'))
+            'or "google"), or the name of a file with style settings. The '
+            'default is pep8 unless a %s file located in one of the parent '
+            'directories of the source file (or current directory for '
+            'stdin)' % style.LOCAL_STYLE))
+  parser.add_argument('--no-local-style',
+                      action='store_true',
+                      help=('Do not search for local style defintion (%s)' %
+                            style.LOCAL_STYLE))
   parser.add_argument('--verify',
                       action='store_true',
                       help='try to verify reformatted code for syntax errors')
@@ -128,10 +134,13 @@ def main(argv):
         original_source.append(py3compat.raw_input())
       except EOFError:
         break
+    style_config = args.style
+    if style_config is None and not args.no_local_style:
+      style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
     sys.stdout.write(yapf_api.FormatCode(
         py3compat.unicode('\n'.join(original_source) + '\n'),
         filename='<stdin>',
-        style_config=args.style,
+        style_config=style_config,
         lines=lines,
         verify=args.verify))
     return 0
@@ -141,6 +150,7 @@ def main(argv):
     raise YapfError('Input filenames did not match any python files')
   FormatFiles(files, lines,
               style_config=args.style,
+              no_local_style=args.no_local_style,
               in_place=args.in_place,
               print_diff=args.diff,
               verify=args.verify)
@@ -149,6 +159,7 @@ def main(argv):
 
 def FormatFiles(filenames, lines,
                 style_config=None,
+                no_local_style=False,
                 in_place=False,
                 print_diff=False,
                 verify=True):
@@ -161,6 +172,8 @@ def FormatFiles(filenames, lines,
       overrides the 'args.lines'. It can be used by third-party code (e.g.,
       IDEs) when reformatting a snippet of code.
     style_config: (string) Style name or file path.
+    no_local_style: (string) If style_config is None don't search for
+      directory-local style configuration.
     in_place: (bool) Modify the files in place.
     print_diff: (bool) Instead of returning the reformatted source, return a
       diff that turns the formatted source into reformatter source.
@@ -168,6 +181,8 @@ def FormatFiles(filenames, lines,
   """
   for filename in filenames:
     logging.info('Reformatting %s', filename)
+    if style_config is None and not no_local_style:
+      style_config = file_resources.GetDefaultStyleForDir(os.path.dirname(filename))
     try:
       reformatted_code, encoding = yapf_api.FormatFile(
           filename, style_config=style_config, lines=lines,
diff --git a/yapf/yapflib/file_resources.py b/yapf/yapflib/file_resources.py
index e34fb2f..f5a3924 100644
--- a/yapf/yapflib/file_resources.py
+++ b/yapf/yapflib/file_resources.py
@@ -23,6 +23,24 @@ import re
 from lib2to3.pgen2 import tokenize
 
 from yapf.yapflib import py3compat
+from yapf.yapflib import style
+
+
+def GetDefaultStyleForDir(dirname):
+  """Return default style name for a given directory.
+
+  Looks for .style.yapf in the parent directories and return the filename if
+  found, otherwise return the glboal default (pep8)."""
+  dirname = os.path.abspath(dirname)
+  while True:
+    style_file = os.path.join(dirname, style.LOCAL_STYLE)
+    if os.path.exists(style_file):
+      return style_file
+    dirname = os.path.dirname(dirname)
+    if not dirname or dirname == os.path.sep:
+      break
+
+  return style.DEFAULT_STYLE
 
 
 def GetCommandLineFiles(command_line_file_list, recursive):
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index 7f9224f..7d1ecbb 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -141,7 +141,6 @@ _STYLE_NAME_TO_FACTORY = dict(
     pep8=CreatePEP8Style,
     chromium=CreateChromiumStyle,
     google=CreateGoogleStyle,
-    yapf=CreateChromiumStyle,  # Shortcut to the YAPF programming style.
 )  # yapf: disable
 
 
@@ -278,8 +277,11 @@ def _CreateStyleFromConfigParser(config):
 
 # The default style - used if yapf is not invoked without specifically
 # requesting a formatting style.
+DEFAULT_STYLE = 'pep8'
 DEFAULT_STYLE_FACTORY = CreatePEP8Style
 
+# The name of the file to use for directory-local style defintion.
+LOCAL_STYLE = '.style.yapf'
 
 # TODO(eliben): For now we're preserving the global presence of a style dict.
 # Refactor this so that the style is passed around through yapf rather than


Test patch:
diff --git a/yapftests/file_resources_test.py b/yapftests/file_resources_test.py
index 3ddade8..a75b92b 100644
--- a/yapftests/file_resources_test.py
+++ b/yapftests/file_resources_test.py
@@ -15,6 +15,7 @@
 """Tests for yapf.file_resources."""
 
 import contextlib
+import os
 import shutil
 import sys
 import tempfile
@@ -34,6 +35,33 @@ def stdout_redirector(stream):  # pylint: disable=invalid-name
     sys.stdout = old_stdout
 
 
+class GetDefaultStyleForDirTest(unittest.TestCase):
+
+  def setUp(self):
+    self.test_tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    shutil.rmtree(self.test_tmpdir)
+
+  def testNoLocalStyle(self):
+    test_file = os.path.join(self.test_tmpdir, 'file.py')
+    style_name = file_resources.GetDefaultStyleForDir(test_file)
+    self.assertEqual(style_name, 'pep8')
+
+  def testWithLocalStyle(self):
+    # Create an empty .style.yapf file in test_tmpdir
+    style_file = os.path.join(self.test_tmpdir, '.style.yapf')
+    open(style_file, 'w').close()
+
+    test_filename = os.path.join(self.test_tmpdir, 'file.py')
+    self.assertEqual(style_file,
+                     file_resources.GetDefaultStyleForDir(test_filename))
+
+    test_filename = os.path.join(self.test_tmpdir, 'dir1', 'file.py')
+    self.assertEqual(style_file,
+                     file_resources.GetDefaultStyleForDir(test_filename))
+
+
 class BufferedByteStream(object):
 
   def __init__(self):
diff --git a/yapftests/style_test.py b/yapftests/style_test.py
index 62b5507..768c076 100644
--- a/yapftests/style_test.py
+++ b/yapftests/style_test.py
@@ -39,7 +39,7 @@ class UtilsTest(unittest.TestCase):
     self.assertEqual(style._BoolConverter('0'), False)
 
 
-def _LooksLikeYapfStyle(cfg):
+def _LooksLikeChromiumStyle(cfg):
   return (
       cfg['INDENT_WIDTH'] == 2 and cfg['BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF']
   )
@@ -64,9 +64,9 @@ class PredefinedStylesByNameTest(unittest.TestCase):
     self.assertTrue(_LooksLikePEP8Style(cfg))
 
   def testGoogleByName(self):
-    for google_name in ('yapf', 'Yapf', 'YAPF'):
+    for google_name in ('google', 'Google', 'GOOGLE'):
       cfg = style.CreateStyleFromConfig(google_name)
-      self.assertTrue(_LooksLikeYapfStyle(cfg))
+      self.assertTrue(_LooksLikeGoogleStyle(cfg))
 
   def testPEP8ByName(self):
     for pep8_name in ('PEP8', 'pep8', 'Pep8'):
@@ -113,15 +113,15 @@ class StyleFromFileTest(unittest.TestCase):
       self.assertTrue(_LooksLikePEP8Style(cfg))
       self.assertEqual(cfg['CONTINUATION_INDENT_WIDTH'], 40)
 
-  def testDefaultBasedOnYapfStyle(self):
+  def testDefaultBasedOnChromiumStyle(self):
     cfg = textwrap.dedent('''\
         [style]
-        based_on_style = yapf
+        based_on_style = chromium
         split_penalty_matching_bracket = 33
         ''')
     with _TempFileContents(self.test_tmpdir, cfg) as f:
       cfg = style.CreateStyleFromConfig(f.name)
-      self.assertTrue(_LooksLikeYapfStyle(cfg))
+      self.assertTrue(_LooksLikeChromiumStyle(cfg))
       self.assertEqual(cfg['SPLIT_PENALTY_MATCHING_BRACKET'], 33)
 
   def testDefaultBasedOnGoogleStyle(self):
@@ -138,25 +138,25 @@ class StyleFromFileTest(unittest.TestCase):
   def testBoolOptionValue(self):
     cfg = textwrap.dedent('''\
         [style]
-        based_on_style = yapf
+        based_on_style = chromium
         SPLIT_BEFORE_NAMED_ASSIGNS=False
         split_before_logical_operator = true
         ''')
     with _TempFileContents(self.test_tmpdir, cfg) as f:
       cfg = style.CreateStyleFromConfig(f.name)
-      self.assertTrue(_LooksLikeYapfStyle(cfg))
+      self.assertTrue(_LooksLikeChromiumStyle(cfg))
       self.assertEqual(cfg['SPLIT_BEFORE_NAMED_ASSIGNS'], False)
       self.assertEqual(cfg['SPLIT_BEFORE_LOGICAL_OPERATOR'], True)
 
   def testStringListOptionValue(self):
     cfg = textwrap.dedent('''\
         [style]
-        based_on_style = yapf
+        based_on_style = chromium
         I18N_FUNCTION_CALL = N_, V_, T_
         ''')
     with _TempFileContents(self.test_tmpdir, cfg) as f:
       cfg = style.CreateStyleFromConfig(f.name)
-      self.assertTrue(_LooksLikeYapfStyle(cfg))
+      self.assertTrue(_LooksLikeChromiumStyle(cfg))
       self.assertEqual(cfg['I18N_FUNCTION_CALL'], ['N_', 'V_', 'T_'])
 
   def testErrorNoStyleFile(self):
@@ -193,7 +193,7 @@ class StyleFromCommandLine(unittest.TestCase):
         '{based_on_style: pep8,'
         ' indent_width: 2,'
         ' blank_line_before_nested_class_or_def: True}')
-    self.assertTrue(_LooksLikeYapfStyle(cfg))
+    self.assertTrue(_LooksLikeChromiumStyle(cfg))
     self.assertEqual(cfg['INDENT_WIDTH'], 2)
 
   def testDefaultBasedOnStyleNotStrict(self):
@@ -201,7 +201,7 @@ class StyleFromCommandLine(unittest.TestCase):
         '{based_on_style : pep8'
         ' ,indent_width=2'
         ' blank_line_before_nested_class_or_def:True}')
-    self.assertTrue(_LooksLikeYapfStyle(cfg))
+    self.assertTrue(_LooksLikeChromiumStyle(cfg))
     self.assertEqual(cfg['INDENT_WIDTH'], 2)
 
   def testDefaultBasedOnStyleBadString(self):
diff --git a/yapftests/yapf_test.py b/yapftests/yapf_test.py
index a5a664a..8fede3d 100644
--- a/yapftests/yapf_test.py
+++ b/yapftests/yapf_test.py
@@ -29,7 +29,7 @@ from yapf.yapflib import yapf_api
 ROOT_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
 
 # Verification is turned off by default, but want to enable it for testing.
-YAPF_BINARY = [sys.executable, '-m', 'yapf', '--verify']
+YAPF_BINARY = [sys.executable, '-m', 'yapf', '--verify', '--no-local-style']
 
 
 class YapfTest(unittest.TestCase):
@@ -155,7 +155,7 @@ class CommandLineTest(unittest.TestCase):
     self.assertIsNone(stderrdata)
     self.assertEqual(reformatted_code.decode('utf-8'), expected_formatted_code)
 
-  def testSetGoogleStyle(self):
+  def testSetChromiumStyle(self):
     unformatted_code = textwrap.dedent(u"""\
         def foo(): # trail
             x = 37
@@ -165,7 +165,7 @@ class CommandLineTest(unittest.TestCase):
           x = 37
         """)
 
-    p = subprocess.Popen(YAPF_BINARY + ['--style=yapf'],
+    p = subprocess.Popen(YAPF_BINARY + ['--style=chromium'],
                          stdout=subprocess.PIPE,
                          stdin=subprocess.PIPE,
                          stderr=subprocess.STDOUT)
@@ -174,7 +174,7 @@ class CommandLineTest(unittest.TestCase):
     self.assertIsNone(stderrdata)
     self.assertEqual(reformatted_code.decode('utf-8'), expected_formatted_code)
 
-  def testSetCustomStyleBasedOnYapf(self):
+  def testSetCustomStyleBasedOnChromium(self):
     unformatted_code = textwrap.dedent(u"""\
         def foo(): # trail
             x = 37
@@ -187,7 +187,7 @@ class CommandLineTest(unittest.TestCase):
     with tempfile.NamedTemporaryFile(dir=self.test_tmpdir, mode='w') as f:
       f.write(textwrap.dedent('''\
           [style]
-          based_on_style = yapf
+          based_on_style = chromium
           SPACES_BEFORE_COMMENT = 4
           '''))
       f.flush()