├── .gitignore ├── .pre-commit-hooks.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── setup.py ├── test_unify.py ├── tox.ini └── unify.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | .eggs/ 4 | .tox/ 5 | .travis-solo/ 6 | MANIFEST 7 | README.html 8 | __pycache__/ 9 | build/ 10 | dist/ 11 | htmlcov/ 12 | unify.egg-info/ 13 | untokenize-0.1-py3.3.egg 14 | .idea/ 15 | .python-version 16 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: unify 2 | name: unify 3 | entry: unify 4 | language: python 5 | types: [python] 6 | require_serial: true 7 | args: ["--in-place"] 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | 9 | install: 10 | - python setup.py --quiet install 11 | 12 | script: 13 | - python test_unify.py 14 | 15 | after_success: 16 | - pip install --quiet coverage 17 | - coverage run test_unify.py 18 | - coverage report --show-missing --omit='*/distutils/*'; true 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2018 Steven Myint 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: 2 | pycodestyle unify.py setup.py 3 | pydocstyle unify.py setup.py 4 | pylint \ 5 | --reports=no \ 6 | --disable=invalid-name \ 7 | --disable=inconsistent-return-statements \ 8 | --rcfile=/dev/null \ 9 | unify.py setup.py 10 | rstcheck README.rst 11 | scspell unify.py setup.py test_unify.py README.rst 12 | 13 | coverage: 14 | @rm -f .coverage 15 | @coverage run test_unify.py 16 | @coverage report 17 | @coverage html 18 | @rm -f .coverage 19 | @python -m webbrowser -n "file://${PWD}/htmlcov/index.html" 20 | 21 | mutant: 22 | @mut.py -t unify -u test_unify -mc 23 | 24 | readme: 25 | @restview --long-description --strict 26 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | unify 3 | ===== 4 | 5 | .. image:: https://travis-ci.org/myint/unify.svg?branch=master 6 | :target: https://travis-ci.org/myint/unify 7 | :alt: Build status 8 | 9 | Modifies strings to all use the same quote where possible. 10 | 11 | 12 | Example 13 | ======= 14 | 15 | After running:: 16 | 17 | $ unify --in-place example.py 18 | 19 | this code 20 | 21 | .. code-block:: python 22 | 23 | x = "abc" 24 | y = 'hello' 25 | 26 | gets formatted into this 27 | 28 | .. code-block:: python 29 | 30 | x = 'abc' 31 | y = 'hello' 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Setup for unify.""" 3 | 4 | from __future__ import unicode_literals 5 | 6 | import ast 7 | 8 | from setuptools import setup 9 | 10 | 11 | def version(): 12 | """Return version string.""" 13 | with open('unify.py') as input_file: 14 | for line in input_file: 15 | if line.startswith('__version__'): 16 | return ast.parse(line).body[0].value.s 17 | return None 18 | 19 | 20 | with open('README.rst') as readme: 21 | setup(name='unify', 22 | version=version(), 23 | description='Modifies strings to all use the same ' 24 | '(single/double) quote where possible.', 25 | long_description=readme.read(), 26 | license='Expat License', 27 | author='Steven Myint', 28 | url='https://github.com/myint/unify', 29 | classifiers=['Intended Audience :: Developers', 30 | 'Environment :: Console', 31 | 'Programming Language :: Python :: 2.6', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'License :: OSI Approved :: MIT License'], 35 | keywords='strings, formatter, style', 36 | py_modules=['unify'], 37 | entry_points={ 38 | 'console_scripts': ['unify = unify:main']}, 39 | install_requires=['untokenize'], 40 | test_suite='test_unify') 41 | -------------------------------------------------------------------------------- /test_unify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Test suite for unify.""" 4 | 5 | from __future__ import unicode_literals 6 | 7 | import contextlib 8 | import io 9 | import tempfile 10 | 11 | try: 12 | # Python 2.6 13 | import unittest2 as unittest 14 | except ImportError: 15 | import unittest 16 | 17 | import unify 18 | 19 | 20 | class TestUnits(unittest.TestCase): 21 | 22 | def test_unify_quotes(self): 23 | self.assertEqual("'foo'", 24 | unify.unify_quotes('"foo"', 25 | preferred_quote="'")) 26 | 27 | self.assertEqual('"foo"', 28 | unify.unify_quotes('"foo"', 29 | preferred_quote='"')) 30 | 31 | self.assertEqual('"foo"', 32 | unify.unify_quotes("'foo'", 33 | preferred_quote='"')) 34 | 35 | def test_unify_quotes_should_avoid_some_cases(self): 36 | self.assertEqual('''"foo's"''', 37 | unify.unify_quotes('''"foo's"''', 38 | preferred_quote="'")) 39 | 40 | self.assertEqual('''"""foo"""''', 41 | unify.unify_quotes('''"""foo"""''', 42 | preferred_quote="'")) 43 | 44 | def test_detect_encoding_with_bad_encoding(self): 45 | with temporary_file('# -*- coding: blah -*-\n') as filename: 46 | self.assertEqual('latin-1', 47 | unify.detect_encoding(filename)) 48 | 49 | def test_format_code(self): 50 | self.assertEqual("x = 'abc' \\\n'next'\n", 51 | unify.format_code('x = "abc" \\\n"next"\n', 52 | preferred_quote="'")) 53 | 54 | def test_format_code_with_backslash_in_comment(self): 55 | self.assertEqual("x = 'abc' #\\\n'next'\n", 56 | unify.format_code('x = "abc" #\\\n"next"\n', 57 | preferred_quote="'")) 58 | 59 | def test_format_code_with_syntax_error(self): 60 | self.assertEqual('foo("abc"\n', 61 | unify.format_code('foo("abc"\n', 62 | preferred_quote="'")) 63 | 64 | 65 | class TestUnitsWithFstrings(unittest.TestCase): 66 | """ Tests for python >= 3.6 fstring handling.""" 67 | 68 | def test_unify_quotes(self): 69 | self.assertEqual("f'foo'", 70 | unify.unify_quotes('f"foo"', 71 | preferred_quote="'")) 72 | 73 | self.assertEqual('f"foo"', 74 | unify.unify_quotes('f"foo"', 75 | preferred_quote='"')) 76 | 77 | self.assertEqual('f"foo"', 78 | unify.unify_quotes("f'foo'", 79 | preferred_quote='"')) 80 | 81 | def test_unify_quotes_should_avoid_some_cases(self): 82 | self.assertEqual('''f"foo's"''', 83 | unify.unify_quotes('''f"foo's"''', 84 | preferred_quote="'")) 85 | 86 | self.assertEqual('''f"""foo"""''', 87 | unify.unify_quotes('''f"""foo"""''', 88 | preferred_quote="'")) 89 | 90 | def test_format_code(self): 91 | self.assertEqual("x = f'abc' \\\nf'next'\n", 92 | unify.format_code('x = f"abc" \\\nf"next"\n', 93 | preferred_quote="'")) 94 | 95 | def test_format_code_with_backslash_in_comment(self): 96 | self.assertEqual("x = f'abc' #\\\nf'next'\n", 97 | unify.format_code('x = f"abc" #\\\nf"next"\n', 98 | preferred_quote="'")) 99 | 100 | def test_format_code_with_syntax_error(self): 101 | self.assertEqual('foo(f"abc"\n', 102 | unify.format_code('foo(f"abc"\n', 103 | preferred_quote="'")) 104 | 105 | 106 | class TestUnitsWithRawStrings(unittest.TestCase): 107 | """Test for r-prefix raw string handling.""" 108 | 109 | def test_unify_quotes(self): 110 | self.assertEqual("r'foo'", 111 | unify.unify_quotes('r"foo"', 112 | preferred_quote="'")) 113 | 114 | self.assertEqual('r"foo"', 115 | unify.unify_quotes('r"foo"', 116 | preferred_quote='"')) 117 | 118 | self.assertEqual('r"foo"', 119 | unify.unify_quotes("r'foo'", 120 | preferred_quote='"')) 121 | 122 | def test_unify_quotes_should_avoid_some_cases(self): 123 | self.assertEqual('''r"foo's"''', 124 | unify.unify_quotes('''r"foo's"''', 125 | preferred_quote="'")) 126 | 127 | self.assertEqual('''r"""\\t"""''', 128 | unify.unify_quotes('''r"""\\t"""''', 129 | preferred_quote="'")) 130 | 131 | def test_format_code(self): 132 | self.assertEqual("x = r'abc' \\\nr'next'\n", 133 | unify.format_code('x = r"abc" \\\nr"next"\n', 134 | preferred_quote="'")) 135 | 136 | def test_format_code_with_backslash_in_comment(self): 137 | self.assertEqual("x = r'abc' #\\\nr'next'\n", 138 | unify.format_code('x = r"abc" #\\\nr"next"\n', 139 | preferred_quote="'")) 140 | 141 | def test_format_code_with_syntax_error(self): 142 | self.assertEqual('foo(r"Tabs \t, new lines \n."\n', 143 | unify.format_code('foo(r"Tabs \t, new lines \n."\n', 144 | preferred_quote="'")) 145 | 146 | 147 | class TestUnitsWithUnicodeStrings(unittest.TestCase): 148 | """Test for u-prefix unicode string handling.""" 149 | 150 | def test_unify_quotes(self): 151 | self.assertEqual("u'foo'", 152 | unify.unify_quotes('u"foo"', 153 | preferred_quote="'")) 154 | 155 | self.assertEqual('u"foo"', 156 | unify.unify_quotes('u"foo"', 157 | preferred_quote='"')) 158 | 159 | self.assertEqual('u"foo"', 160 | unify.unify_quotes("u'foo'", 161 | preferred_quote='"')) 162 | 163 | def test_unify_quotes_should_avoid_some_cases(self): 164 | self.assertEqual('''u"foo's"''', 165 | unify.unify_quotes('''u"foo's"''', 166 | preferred_quote="'")) 167 | 168 | self.assertEqual('''u"""foo"""''', 169 | unify.unify_quotes('''u"""foo"""''', 170 | preferred_quote="'")) 171 | 172 | def test_format_code(self): 173 | self.assertEqual("x = u'abc' \\\nu'next'\n", 174 | unify.format_code('x = u"abc" \\\nu"next"\n', 175 | preferred_quote="'")) 176 | 177 | def test_format_code_with_backslash_in_comment(self): 178 | self.assertEqual("x = u'abc' #\\\nu'next'\n", 179 | unify.format_code('x = u"abc" #\\\nu"next"\n', 180 | preferred_quote="'")) 181 | 182 | def test_format_code_with_syntax_error(self): 183 | self.assertEqual('foo(u"abc"\n', 184 | unify.format_code('foo(u"abc"\n', 185 | preferred_quote="'")) 186 | 187 | 188 | class TestUnitsWithByteStrings(unittest.TestCase): 189 | """ Tests for python3 byte string handling.""" 190 | 191 | def test_unify_quotes(self): 192 | self.assertEqual("b'foo'", 193 | unify.unify_quotes('b"foo"', 194 | preferred_quote="'")) 195 | 196 | self.assertEqual('b"foo"', 197 | unify.unify_quotes('b"foo"', 198 | preferred_quote='"')) 199 | 200 | self.assertEqual('b"foo"', 201 | unify.unify_quotes("b'foo'", 202 | preferred_quote='"')) 203 | 204 | def test_unify_quotes_should_avoid_some_cases(self): 205 | self.assertEqual('''b"foo's"''', 206 | unify.unify_quotes('''b"foo's"''', 207 | preferred_quote="'")) 208 | 209 | self.assertEqual('''b"""foo"""''', 210 | unify.unify_quotes('''b"""foo"""''', 211 | preferred_quote="'")) 212 | 213 | def test_format_code(self): 214 | self.assertEqual("x = b'abc' \\\nb'next'\n", 215 | unify.format_code('x = b"abc" \\\nb"next"\n', 216 | preferred_quote="'")) 217 | 218 | def test_format_code_with_backslash_in_comment(self): 219 | self.assertEqual("x = b'abc' #\\\nb'next'\n", 220 | unify.format_code('x = b"abc" #\\\nb"next"\n', 221 | preferred_quote="'")) 222 | 223 | def test_format_code_with_syntax_error(self): 224 | self.assertEqual('foo(b"abc"\n', 225 | unify.format_code('foo(b"abc"\n', 226 | preferred_quote="'")) 227 | 228 | 229 | class TestSystem(unittest.TestCase): 230 | 231 | def test_diff(self): 232 | with temporary_file('''\ 233 | if True: 234 | x = "abc" 235 | ''') as filename: 236 | output_file = io.StringIO() 237 | self.assertEqual( 238 | unify._main(argv=['my_fake_program', filename], 239 | standard_out=output_file, 240 | standard_error=None), 241 | None, 242 | ) 243 | self.assertEqual('''\ 244 | @@ -1,2 +1,2 @@ 245 | if True: 246 | - x = "abc" 247 | + x = 'abc' 248 | ''', '\n'.join(output_file.getvalue().split('\n')[2:])) 249 | 250 | def test_check_only(self): 251 | with temporary_file('''\ 252 | if True: 253 | x = "abc" 254 | ''') as filename: 255 | output_file = io.StringIO() 256 | self.assertEqual( 257 | unify._main(argv=['my_fake_program', '--check-only', filename], 258 | standard_out=output_file, 259 | standard_error=None), 260 | 1, 261 | ) 262 | self.assertEqual('''\ 263 | @@ -1,2 +1,2 @@ 264 | if True: 265 | - x = "abc" 266 | + x = 'abc' 267 | ''', '\n'.join(output_file.getvalue().split('\n')[2:])) 268 | 269 | def test_diff_with_empty_file(self): 270 | with temporary_file('') as filename: 271 | output_file = io.StringIO() 272 | unify._main(argv=['my_fake_program', filename], 273 | standard_out=output_file, 274 | standard_error=None) 275 | self.assertEqual( 276 | '', 277 | output_file.getvalue()) 278 | 279 | def test_diff_with_missing_file(self): 280 | output_file = io.StringIO() 281 | non_existent_filename = '/non_existent_file_92394492929' 282 | 283 | self.assertEqual( 284 | 1, 285 | unify._main(argv=['my_fake_program', 286 | '/non_existent_file_92394492929'], 287 | standard_out=None, 288 | standard_error=output_file)) 289 | 290 | self.assertIn(non_existent_filename, output_file.getvalue()) 291 | 292 | def test_in_place(self): 293 | with temporary_file('''\ 294 | if True: 295 | x = "abc" 296 | ''') as filename: 297 | output_file = io.StringIO() 298 | self.assertEqual( 299 | unify._main(argv=['my_fake_program', '--in-place', filename], 300 | standard_out=output_file, 301 | standard_error=None), 302 | None, 303 | ) 304 | with open(filename) as f: 305 | self.assertEqual('''\ 306 | if True: 307 | x = 'abc' 308 | ''', f.read()) 309 | 310 | def test_in_place_precedence_over_check_only(self): 311 | with temporary_file('''\ 312 | if True: 313 | x = "abc" 314 | ''') as filename: 315 | output_file = io.StringIO() 316 | self.assertEqual( 317 | unify._main(argv=['my_fake_program', 318 | '--in-place', 319 | '--check-only', 320 | filename], 321 | standard_out=output_file, 322 | standard_error=None), 323 | None, 324 | ) 325 | with open(filename) as f: 326 | self.assertEqual('''\ 327 | if True: 328 | x = 'abc' 329 | ''', f.read()) 330 | 331 | def test_ignore_hidden_directories(self): 332 | with temporary_directory() as directory: 333 | with temporary_directory(prefix='.', 334 | directory=directory) as inner_directory: 335 | 336 | with temporary_file("""\ 337 | if True: 338 | x = "abc" 339 | """, directory=inner_directory): 340 | 341 | output_file = io.StringIO() 342 | self.assertEqual( 343 | unify._main(argv=['my_fake_program', 344 | '--recursive', 345 | directory], 346 | standard_out=output_file, 347 | standard_error=None), 348 | None, 349 | ) 350 | self.assertEqual( 351 | '', 352 | output_file.getvalue().strip()) 353 | 354 | 355 | @contextlib.contextmanager 356 | def temporary_file(contents, directory='.', prefix=''): 357 | """Write contents to temporary file and yield it.""" 358 | f = tempfile.NamedTemporaryFile(suffix='.py', prefix=prefix, 359 | delete=False, dir=directory) 360 | try: 361 | f.write(contents.encode()) 362 | f.close() 363 | yield f.name 364 | finally: 365 | import os 366 | os.remove(f.name) 367 | 368 | 369 | @contextlib.contextmanager 370 | def temporary_directory(directory='.', prefix=''): 371 | """Create temporary directory and yield its path.""" 372 | temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) 373 | try: 374 | yield temp_directory 375 | finally: 376 | import shutil 377 | shutil.rmtree(temp_directory) 378 | 379 | 380 | if __name__ == '__main__': 381 | unittest.main() 382 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py2.6,py2.7,py3.4,py3.5,py3.6 3 | 4 | [testenv] 5 | commands=python setup.py test 6 | -------------------------------------------------------------------------------- /unify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2013-2018 Steven Myint 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Modifies strings to all use the same quote where possible.""" 26 | 27 | from __future__ import print_function 28 | from __future__ import unicode_literals 29 | 30 | import io 31 | import os 32 | import signal 33 | import sys 34 | import tokenize 35 | 36 | import untokenize 37 | 38 | 39 | __version__ = '0.5' 40 | 41 | 42 | try: 43 | unicode 44 | except NameError: 45 | unicode = str 46 | 47 | 48 | def format_code(source, preferred_quote="'"): 49 | """Return source code with quotes unified.""" 50 | try: 51 | return _format_code(source, preferred_quote) 52 | except (tokenize.TokenError, IndentationError): 53 | return source 54 | 55 | 56 | def _format_code(source, preferred_quote): 57 | """Return source code with quotes unified.""" 58 | if not source: 59 | return source 60 | 61 | modified_tokens = [] 62 | 63 | sio = io.StringIO(source) 64 | for (token_type, 65 | token_string, 66 | start, 67 | end, 68 | line) in tokenize.generate_tokens(sio.readline): 69 | 70 | if token_type == tokenize.STRING: 71 | token_string = unify_quotes(token_string, 72 | preferred_quote=preferred_quote) 73 | 74 | modified_tokens.append( 75 | (token_type, token_string, start, end, line)) 76 | 77 | return untokenize.untokenize(modified_tokens) 78 | 79 | 80 | def unify_quotes(token_string, preferred_quote): 81 | """Return string with quotes changed to preferred_quote if possible.""" 82 | bad_quote = {'"': "'", 83 | "'": '"'}[preferred_quote] 84 | 85 | allowed_starts = { 86 | '': bad_quote, 87 | 'f': 'f' + bad_quote, 88 | 'r': 'r' + bad_quote, 89 | 'u': 'u' + bad_quote, 90 | 'b': 'b' + bad_quote 91 | } 92 | 93 | if not any(token_string.startswith(start) 94 | for start in allowed_starts.values()): 95 | return token_string 96 | 97 | if token_string.count(bad_quote) != 2: 98 | return token_string 99 | 100 | if preferred_quote in token_string: 101 | return token_string 102 | 103 | assert token_string.endswith(bad_quote) 104 | assert len(token_string) >= 2 105 | for prefix, start in allowed_starts.items(): 106 | if token_string.startswith(start): 107 | chars_to_strip_from_front = len(start) 108 | return '{prefix}{preferred_quote}{token}{preferred_quote}'.format( 109 | prefix=prefix, 110 | preferred_quote=preferred_quote, 111 | token=token_string[chars_to_strip_from_front:-1] 112 | ) 113 | 114 | 115 | def open_with_encoding(filename, encoding, mode='r'): 116 | """Return opened file with a specific encoding.""" 117 | return io.open(filename, mode=mode, encoding=encoding, 118 | newline='') # Preserve line endings 119 | 120 | 121 | def detect_encoding(filename): 122 | """Return file encoding.""" 123 | try: 124 | with open(filename, 'rb') as input_file: 125 | from lib2to3.pgen2 import tokenize as lib2to3_tokenize 126 | encoding = lib2to3_tokenize.detect_encoding(input_file.readline)[0] 127 | 128 | # Check for correctness of encoding. 129 | with open_with_encoding(filename, encoding) as input_file: 130 | input_file.read() 131 | 132 | return encoding 133 | except (SyntaxError, LookupError, UnicodeDecodeError): 134 | return 'latin-1' 135 | 136 | 137 | def format_file(filename, args, standard_out): 138 | """Run format_code() on a file. 139 | 140 | Returns `True` if any changes are needed and they are not being done 141 | in-place. 142 | 143 | """ 144 | encoding = detect_encoding(filename) 145 | with open_with_encoding(filename, encoding=encoding) as input_file: 146 | source = input_file.read() 147 | formatted_source = format_code( 148 | source, 149 | preferred_quote=args.quote) 150 | 151 | if source != formatted_source: 152 | if args.in_place: 153 | with open_with_encoding(filename, mode='w', 154 | encoding=encoding) as output_file: 155 | output_file.write(formatted_source) 156 | else: 157 | import difflib 158 | diff = difflib.unified_diff( 159 | source.splitlines(), 160 | formatted_source.splitlines(), 161 | 'before/' + filename, 162 | 'after/' + filename, 163 | lineterm='') 164 | standard_out.write('\n'.join(list(diff) + [''])) 165 | 166 | return True 167 | 168 | return False 169 | 170 | 171 | def _main(argv, standard_out, standard_error): 172 | """Run quotes unifying on files. 173 | 174 | Returns `1` if any quoting changes are still needed, otherwise 175 | `None`. 176 | 177 | """ 178 | import argparse 179 | parser = argparse.ArgumentParser(description=__doc__, prog='unify') 180 | parser.add_argument('-i', '--in-place', action='store_true', 181 | help='make changes to files instead of printing diffs') 182 | parser.add_argument('-c', '--check-only', action='store_true', 183 | help='exit with a status code of 1 if any changes are' 184 | ' still needed') 185 | parser.add_argument('-r', '--recursive', action='store_true', 186 | help='drill down directories recursively') 187 | parser.add_argument('--quote', help='preferred quote', choices=["'", '"'], 188 | default="'") 189 | parser.add_argument('--version', action='version', 190 | version='%(prog)s ' + __version__) 191 | parser.add_argument('files', nargs='+', 192 | help='files to format') 193 | 194 | args = parser.parse_args(argv[1:]) 195 | 196 | filenames = list(set(args.files)) 197 | changes_needed = False 198 | failure = False 199 | while filenames: 200 | name = filenames.pop(0) 201 | if args.recursive and os.path.isdir(name): 202 | for root, directories, children in os.walk(unicode(name)): 203 | filenames += [os.path.join(root, f) for f in children 204 | if f.endswith('.py') and 205 | not f.startswith('.')] 206 | directories[:] = [d for d in directories 207 | if not d.startswith('.')] 208 | else: 209 | try: 210 | if format_file(name, args=args, standard_out=standard_out): 211 | changes_needed = True 212 | except IOError as exception: 213 | print(unicode(exception), file=standard_error) 214 | failure = True 215 | 216 | if failure or (args.check_only and changes_needed): 217 | return 1 218 | 219 | 220 | def main(): # pragma: no cover 221 | """Return exit status.""" 222 | try: 223 | # Exit on broken pipe. 224 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 225 | except AttributeError: 226 | # SIGPIPE is not available on Windows. 227 | pass 228 | 229 | try: 230 | return _main(sys.argv, 231 | standard_out=sys.stdout, 232 | standard_error=sys.stderr) 233 | except KeyboardInterrupt: 234 | return 2 235 | 236 | 237 | if __name__ == '__main__': 238 | sys.exit(main()) # pragma: no cover 239 | --------------------------------------------------------------------------------