├── AUTHORS.rst ├── .gitignore ├── MANIFEST.in ├── tox.ini ├── .travis.yml ├── Makefile ├── setup.py ├── README.rst ├── test_acid.py ├── pyformat.py └── test_pyformat.py /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Author 2 | ------ 3 | - Steven Myint (https://github.com/myint) 4 | 5 | Patches 6 | ------- 7 | - generalov (https://github.com/generalov) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | *.pyc 3 | MANIFEST 4 | README.html 5 | __pycache__/ 6 | .tox/ 7 | .travis-solo/ 8 | build/ 9 | dist/ 10 | htmlcov/ 11 | *.egg*/ 12 | .coverage 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include README.rst 3 | include test_pyformat.py 4 | 5 | exclude .travis.yml 6 | exclude Makefile 7 | exclude test_acid.py 8 | exclude tox.ini 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py27,py34 3 | 4 | [testenv] 5 | commands= 6 | python test_pyformat.py 7 | pyformat pyformat.py 8 | python test_acid.py setup.py 9 | deps= 10 | autoflake>=0.6.1 11 | autopep8>=0.9.7 12 | docformatter>=0.5.6 13 | unify>=0.2 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "2.7" 7 | - "3.4" 8 | - "3.5" 9 | - "3.6" 10 | 11 | env: 12 | - 13 | - LANG= 14 | - PYTHONIOENCODING=ascii 15 | - PYTHONIOENCODING=utf-8 16 | - PYTHONOPTIMIZE=2 17 | 18 | install: 19 | - python setup.py --quiet install 20 | 21 | script: 22 | - python test_pyformat.py 23 | - pyformat pyformat.py 24 | - python test_acid.py setup.py 25 | 26 | - pycodestyle pyformat.py 27 | - pip install pyflakes; pyflakes pyformat.py 28 | 29 | after_success: 30 | - pip install --quiet coverage 31 | - make coverage 32 | 33 | - pip install --quiet coveralls 34 | - coveralls 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | check: 2 | pycodestyle pyformat.py setup.py 3 | pydocstyle pyformat.py setup.py 4 | pylint \ 5 | --rcfile=/dev/null \ 6 | --errors-only \ 7 | pyformat.py setup.py 8 | check-manifest 9 | rstcheck README.rst 10 | scspell pyformat.py setup.py test_pyformat.py README.rst 11 | 12 | coverage: 13 | @coverage erase 14 | @PYFORMAT_COVERAGE=1 coverage run \ 15 | --branch --parallel-mode \ 16 | --omit='*/distutils/*,*/site-packages/*' \ 17 | test_pyformat.py 18 | @coverage combine 19 | @coverage report 20 | 21 | open_coverage: coverage 22 | @coverage html 23 | @python -m webbrowser -n "file://${PWD}/htmlcov/index.html" 24 | 25 | mutant: 26 | @mut.py -t pyformat -u test_pyformat -mc 27 | 28 | readme: 29 | @restview --long-description --strict 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Setup for pyformat.""" 4 | 5 | import ast 6 | 7 | from setuptools import setup 8 | 9 | 10 | def version(): 11 | """Return version string.""" 12 | with open('pyformat.py') as input_file: 13 | for line in input_file: 14 | if line.startswith('__version__'): 15 | return ast.parse(line).body[0].value.s 16 | 17 | 18 | with open('README.rst') as readme: 19 | setup(name='pyformat', 20 | version=version(), 21 | description='Formats Python code to follow a consistent style.', 22 | long_description=readme.read(), 23 | license='Expat License', 24 | author='Steven Myint', 25 | url='https://github.com/myint/pyformat', 26 | classifiers=[ 27 | 'Intended Audience :: Developers', 28 | 'Environment :: Console', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | 'Topic :: Software Development :: Quality Assurance'], 38 | keywords='beautify, code, format, formatter, reformat, style', 39 | py_modules=['pyformat'], 40 | zip_safe=False, 41 | install_requires=['autoflake>=0.6.6', 42 | 'autopep8>=1.2.2', 43 | 'docformatter==0.7', 44 | 'unify>=0.2'], 45 | entry_points={ 46 | 'console_scripts': ['pyformat = pyformat:main']}, 47 | test_suite='test_pyformat') 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | pyformat 3 | ======== 4 | 5 | .. image:: https://travis-ci.org/myint/pyformat.svg?branch=master 6 | :target: https://travis-ci.org/myint/pyformat 7 | :alt: Build status 8 | 9 | *pyformat* formats Python code to follow a consistent style. 10 | 11 | 12 | Features 13 | ======== 14 | 15 | - Formats code to follow the PEP 8 style guide (using autopep8_). 16 | - Removes unused imports (using autoflake_). 17 | - Formats docstrings to follow PEP 257 (using docformatter_). 18 | - Makes strings all use the same type of quote where possible (using unify_). 19 | 20 | 21 | Installation 22 | ============ 23 | 24 | From pip:: 25 | 26 | $ pip install --upgrade pyformat 27 | 28 | 29 | Example 30 | ======= 31 | 32 | After running:: 33 | 34 | $ pyformat --in-place example.py 35 | 36 | This code: 37 | 38 | .. code-block:: python 39 | 40 | def launch_rocket (): 41 | 42 | 43 | 44 | """Launch 45 | the 46 | rocket. Go colonize space.""" 47 | 48 | def factorial(x): 49 | ''' 50 | 51 | Return x factorial. 52 | 53 | This uses math.factorial. 54 | 55 | ''' 56 | import math 57 | import re 58 | import os 59 | return math.factorial( x ); 60 | def print_factorial(x): 61 | """Print x factorial""" 62 | print( factorial(x) ) 63 | def main(): 64 | """Main 65 | function""" 66 | print_factorial(5) 67 | if factorial(10): 68 | launch_rocket() 69 | 70 | Gets formatted into this: 71 | 72 | .. code-block:: python 73 | 74 | def launch_rocket(): 75 | """Launch the rocket. 76 | 77 | Go colonize space. 78 | 79 | """ 80 | 81 | 82 | def factorial(x): 83 | """Return x factorial. 84 | 85 | This uses math.factorial. 86 | 87 | """ 88 | import math 89 | return math.factorial(x) 90 | 91 | 92 | def print_factorial(x): 93 | """Print x factorial.""" 94 | print(factorial(x)) 95 | 96 | 97 | def main(): 98 | """Main function.""" 99 | print_factorial(5) 100 | if factorial(10): 101 | launch_rocket() 102 | 103 | 104 | .. _autoflake: https://github.com/myint/autoflake 105 | .. _autopep8: https://github.com/hhatto/autopep8 106 | .. _docformatter: https://github.com/myint/docformatter 107 | .. _unify: https://github.com/myint/unify 108 | -------------------------------------------------------------------------------- /test_acid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Test that pyformat runs without crashing on various Python files.""" 3 | 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import sys 9 | import subprocess 10 | 11 | 12 | ROOT_PATH = os.path.abspath(os.path.dirname(__file__)) 13 | PYFORMAT_BIN = os.path.join(ROOT_PATH, 'pyformat.py') 14 | 15 | import autopep8 16 | 17 | if sys.stdout.isatty(): 18 | YELLOW = '\x1b[33m' 19 | END = '\x1b[0m' 20 | else: 21 | YELLOW = '' 22 | END = '' 23 | 24 | 25 | def colored(text, color): 26 | """Return color coded text.""" 27 | return color + text + END 28 | 29 | 30 | def readlines(filename): 31 | """Return contents of file as a list of lines.""" 32 | with autopep8.open_with_encoding( 33 | filename, 34 | encoding=autopep8.detect_encoding(filename)) as f: 35 | return f.readlines() 36 | 37 | 38 | def diff(before, after): 39 | """Return diff of two files.""" 40 | import difflib 41 | return ''.join(difflib.unified_diff( 42 | readlines(before), 43 | readlines(after), 44 | before, 45 | after)) 46 | 47 | 48 | def run(filename, verbose=False, options=None): 49 | """Run pyformat on file at filename. 50 | 51 | Return True on success. 52 | 53 | """ 54 | if not options: 55 | options = [] 56 | 57 | import test_pyformat 58 | with test_pyformat.temporary_directory() as temp_directory: 59 | temp_filename = os.path.join(temp_directory, 60 | os.path.basename(filename)) 61 | import shutil 62 | shutil.copyfile(filename, temp_filename) 63 | 64 | if 0 != subprocess.call([PYFORMAT_BIN, '--in-place', temp_filename] + 65 | options): 66 | sys.stderr.write('pyformat crashed on ' + filename + '\n') 67 | return False 68 | 69 | try: 70 | file_diff = diff(filename, temp_filename) 71 | if verbose: 72 | sys.stderr.write(file_diff) 73 | 74 | if check_syntax(filename): 75 | try: 76 | check_syntax(temp_filename, raise_error=True) 77 | except (SyntaxError, TypeError, 78 | UnicodeDecodeError) as exception: 79 | sys.stderr.write('pyformat broke ' + filename + '\n' + 80 | str(exception) + '\n') 81 | return False 82 | except IOError as exception: 83 | sys.stderr.write(str(exception) + '\n') 84 | 85 | return True 86 | 87 | 88 | def check_syntax(filename, raise_error=False): 89 | """Return True if syntax is okay.""" 90 | with autopep8.open_with_encoding( 91 | filename, 92 | encoding=autopep8.detect_encoding(filename)) as input_file: 93 | try: 94 | compile(input_file.read(), '', 'exec', dont_inherit=True) 95 | return True 96 | except (SyntaxError, TypeError, UnicodeDecodeError): 97 | if raise_error: 98 | raise 99 | else: 100 | return False 101 | 102 | 103 | def process_args(): 104 | """Return processed arguments (options and positional arguments).""" 105 | import argparse 106 | parser = argparse.ArgumentParser() 107 | 108 | parser.add_argument('--aggressive', action='store_true', 109 | help='pass to the pyformat "--aggressive" option') 110 | 111 | parser.add_argument('-v', '--verbose', action='store_true', 112 | help='print verbose messages') 113 | 114 | parser.add_argument('files', nargs='*', help='files to format') 115 | 116 | return parser.parse_args() 117 | 118 | 119 | def check(args): 120 | """Run recursively run pyformat on directory of files. 121 | 122 | Return False if the fix results in broken syntax. 123 | 124 | """ 125 | if args.files: 126 | dir_paths = args.files 127 | else: 128 | dir_paths = [path for path in sys.path 129 | if os.path.isdir(path)] 130 | 131 | options = [] 132 | if args.aggressive: 133 | options.append('--aggressive') 134 | 135 | filenames = dir_paths 136 | completed_filenames = set() 137 | 138 | while filenames: 139 | try: 140 | name = os.path.realpath(filenames.pop(0)) 141 | if not os.path.exists(name): 142 | # Invalid symlink. 143 | continue 144 | 145 | if name in completed_filenames: 146 | sys.stderr.write( 147 | colored( 148 | '---> Skipping previously tested ' + name + '\n', 149 | YELLOW)) 150 | continue 151 | else: 152 | completed_filenames.update(name) 153 | 154 | if os.path.isdir(name): 155 | for root, directories, children in os.walk('{}'.format(name)): 156 | filenames += [os.path.join(root, f) for f in children 157 | if f.endswith('.py') and 158 | not f.startswith('.')] 159 | 160 | directories[:] = [d for d in directories 161 | if not d.startswith('.')] 162 | else: 163 | verbose_message = '---> Testing with ' + name 164 | sys.stderr.write(colored(verbose_message + '\n', YELLOW)) 165 | 166 | if not run(os.path.join(name), verbose=args.verbose, 167 | options=options): 168 | return False 169 | except (UnicodeDecodeError, UnicodeEncodeError) as exception: 170 | # Ignore annoying codec problems on Python 2. 171 | print(exception, file=sys.stderr) 172 | continue 173 | 174 | return True 175 | 176 | 177 | def main(): 178 | """Run main.""" 179 | return 0 if check(process_args()) else 1 180 | 181 | 182 | if __name__ == '__main__': 183 | try: 184 | sys.exit(main()) 185 | except KeyboardInterrupt: 186 | sys.exit(1) 187 | -------------------------------------------------------------------------------- /pyformat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2013-2017 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 included 14 | # 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 NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | """Formats Python code to follow a consistent style.""" 25 | 26 | from __future__ import absolute_import 27 | from __future__ import division 28 | from __future__ import print_function 29 | from __future__ import unicode_literals 30 | 31 | import io 32 | import signal 33 | import sys 34 | 35 | import autoflake 36 | import autopep8 37 | import docformatter 38 | import unify 39 | 40 | 41 | __version__ = '1.0' 42 | 43 | 44 | def formatters(aggressive, apply_config, filename='', 45 | remove_all_unused_imports=False, remove_unused_variables=False): 46 | """Return list of code formatters.""" 47 | if aggressive: 48 | yield lambda code: autoflake.fix_code( 49 | code, 50 | remove_all_unused_imports=remove_all_unused_imports, 51 | remove_unused_variables=remove_unused_variables) 52 | 53 | autopep8_options = autopep8.parse_args( 54 | [filename] + int(aggressive) * ['--aggressive'], 55 | apply_config=apply_config) 56 | else: 57 | autopep8_options = autopep8.parse_args( 58 | [filename], apply_config=apply_config) 59 | 60 | yield lambda code: autopep8.fix_code(code, options=autopep8_options) 61 | yield docformatter.format_code 62 | yield unify.format_code 63 | 64 | 65 | def format_code(source, aggressive=False, apply_config=False, filename='', 66 | remove_all_unused_imports=False, 67 | remove_unused_variables=False): 68 | """Return formatted source code.""" 69 | formatted_source = source 70 | 71 | for fix in formatters( 72 | aggressive, apply_config, filename, 73 | remove_all_unused_imports, remove_unused_variables): 74 | formatted_source = fix(formatted_source) 75 | 76 | return formatted_source 77 | 78 | 79 | def format_file(filename, args, standard_out): 80 | """Run format_code() on a file. 81 | 82 | Return True if the new formatting differs from the original. 83 | 84 | """ 85 | encoding = autopep8.detect_encoding(filename) 86 | with autopep8.open_with_encoding(filename, 87 | encoding=encoding) as input_file: 88 | source = input_file.read() 89 | 90 | if not source: 91 | return False 92 | 93 | formatted_source = format_code( 94 | source, 95 | aggressive=args.aggressive, 96 | apply_config=args.config, 97 | filename=filename, 98 | remove_all_unused_imports=args.remove_all_unused_imports, 99 | remove_unused_variables=args.remove_unused_variables) 100 | 101 | if source != formatted_source: 102 | if args.in_place: 103 | with autopep8.open_with_encoding(filename, mode='w', 104 | encoding=encoding) as output_file: 105 | output_file.write(formatted_source) 106 | else: 107 | diff = autopep8.get_diff_text( 108 | io.StringIO(source).readlines(), 109 | io.StringIO(formatted_source).readlines(), 110 | filename) 111 | standard_out.write(''.join(diff)) 112 | 113 | return True 114 | 115 | return False 116 | 117 | 118 | def _format_file(parameters): 119 | """Helper function for optionally running format_file() in parallel.""" 120 | (filename, args, _, standard_error) = parameters 121 | 122 | standard_error = standard_error or sys.stderr 123 | 124 | if args.verbose: 125 | print('{0}: '.format(filename), end='', file=standard_error) 126 | 127 | try: 128 | changed = format_file(*parameters[:-1]) 129 | except IOError as exception: 130 | print('{}'.format(exception), file=standard_error) 131 | return (False, True) 132 | except KeyboardInterrupt: # pragma: no cover 133 | return (False, True) # pragma: no cover 134 | 135 | if args.verbose: 136 | print('changed' if changed else 'unchanged', file=standard_error) 137 | 138 | return (changed, False) 139 | 140 | 141 | def format_multiple_files(filenames, args, standard_out, standard_error): 142 | """Format files and return booleans (any_changes, any_errors). 143 | 144 | Optionally format files recursively. 145 | 146 | """ 147 | filenames = autopep8.find_files(list(filenames), 148 | args.recursive, 149 | args.exclude_patterns) 150 | if args.jobs > 1: 151 | import multiprocessing 152 | pool = multiprocessing.Pool(args.jobs) 153 | 154 | # We pass neither standard_out nor standard_error into "_format_file()" 155 | # since multiprocessing cannot serialize io. 156 | result = pool.map(_format_file, 157 | [(name, args, None, None) for name in filenames]) 158 | else: 159 | result = [_format_file((name, args, standard_out, standard_error)) 160 | for name in filenames] 161 | 162 | return (any(changed_and_error[0] for changed_and_error in result), 163 | any(changed_and_error[1] for changed_and_error in result)) 164 | 165 | 166 | def parse_args(argv): 167 | """Return parsed arguments.""" 168 | import argparse 169 | parser = argparse.ArgumentParser(description=__doc__, prog='pyformat') 170 | parser.add_argument('-i', '--in-place', action='store_true', 171 | help='make changes to files instead of printing diffs') 172 | parser.add_argument('-r', '--recursive', action='store_true', 173 | help='drill down directories recursively') 174 | parser.add_argument('-a', '--aggressive', action='count', default=0, 175 | help='use more aggressive formatters') 176 | parser.add_argument('--remove-all-unused-imports', action='store_true', 177 | help='remove all unused imports, ' 178 | 'not just standard library ' 179 | '(requires "aggressive")') 180 | parser.add_argument('--remove-unused-variables', action='store_true', 181 | help='remove unused variables (requires "aggressive")') 182 | parser.add_argument('-j', '--jobs', type=int, metavar='n', default=1, 183 | help='number of parallel jobs; ' 184 | 'match CPU count if value is less than 1') 185 | parser.add_argument('-v', '--verbose', action='store_true', 186 | help='print verbose messages') 187 | parser.add_argument('--exclude', action='append', 188 | dest='exclude_patterns', default=[], metavar='pattern', 189 | help='exclude files this pattern; ' 190 | 'specify this multiple times for multiple ' 191 | 'patterns') 192 | parser.add_argument('--no-config', action='store_false', dest='config', 193 | help="don't look for and apply local configuration " 194 | 'files; if not passed, defaults are updated with ' 195 | "any config files in the project's root " 196 | 'directory') 197 | parser.add_argument('--version', action='version', 198 | version='%(prog)s ' + __version__) 199 | parser.add_argument('files', nargs='+', help='files to format') 200 | 201 | args = parser.parse_args(argv[1:]) 202 | 203 | if args.jobs < 1: 204 | import multiprocessing 205 | args.jobs = multiprocessing.cpu_count() 206 | 207 | return args 208 | 209 | 210 | def _main(argv, standard_out, standard_error): 211 | """Internal main entry point. 212 | 213 | Return exit status. 0 means no error. 214 | 215 | """ 216 | args = parse_args(argv) 217 | 218 | if args.jobs > 1 and not args.in_place: 219 | print('parallel jobs requires --in-place', 220 | file=standard_error) 221 | return 2 222 | 223 | if not args.aggressive: 224 | if args.remove_all_unused_imports: 225 | print('--remove-all-unused-imports requires --aggressive', 226 | file=standard_error) 227 | return 2 228 | 229 | if args.remove_unused_variables: 230 | print('--remove-unused-variables requires --aggressive', 231 | file=standard_error) 232 | return 2 233 | 234 | changed_and_error = format_multiple_files(set(args.files), 235 | args, 236 | standard_out, 237 | standard_error) 238 | return 1 if changed_and_error[1] else 0 239 | 240 | 241 | def main(): 242 | """Main entry point.""" 243 | try: 244 | # Exit on broken pipe. 245 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) 246 | except AttributeError: # pragma: no cover 247 | # SIGPIPE is not available on Windows. 248 | pass 249 | 250 | try: 251 | return _main(sys.argv, 252 | standard_out=sys.stdout, 253 | standard_error=sys.stderr) 254 | except KeyboardInterrupt: # pragma: no cover 255 | return 2 # pragma: no cover 256 | 257 | 258 | if __name__ == '__main__': 259 | sys.exit(main()) 260 | -------------------------------------------------------------------------------- /test_pyformat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Test suite for pyformat.""" 5 | 6 | from __future__ import unicode_literals 7 | 8 | import contextlib 9 | import io 10 | import os 11 | import shutil 12 | import subprocess 13 | import sys 14 | import tempfile 15 | import unittest 16 | 17 | import pyformat 18 | 19 | 20 | ROOT_DIRECTORY = os.path.abspath(os.path.dirname(__file__)) 21 | 22 | 23 | if ( 24 | 'PYFORMAT_COVERAGE' in os.environ and 25 | int(os.environ['PYFORMAT_COVERAGE']) 26 | ): 27 | PYFORMAT_COMMAND = ['coverage', 'run', '--branch', '--parallel', 28 | '--omit=*/distutils/*,*/site-packages/*', 29 | os.path.join(ROOT_DIRECTORY, 'pyformat.py')] 30 | else: 31 | # We need to specify the executable to make sure the correct Python 32 | # interpreter gets used. 33 | PYFORMAT_COMMAND = [sys.executable, 34 | os.path.join( 35 | ROOT_DIRECTORY, 36 | 'pyformat.py')] # pragma: no cover 37 | 38 | 39 | class TestUnits(unittest.TestCase): 40 | 41 | def test_format_code(self): 42 | self.assertEqual( 43 | "x = 'abc' \\\n 'next'\n", 44 | pyformat.format_code( 45 | 'x = "abc" \\\n"next"\n')) 46 | 47 | def test_format_code_with_aggressive(self): 48 | self.assertEqual( 49 | 'True\n', 50 | pyformat.format_code( 51 | 'import os\nTrue\n', 52 | aggressive=True)) 53 | 54 | def test_format_code_without_aggressive(self): 55 | self.assertEqual( 56 | 'import os\nTrue\n', 57 | pyformat.format_code( 58 | 'import os\nTrue\n', 59 | aggressive=False)) 60 | 61 | def test_format_code_with_unicode(self): 62 | self.assertEqual( 63 | "x = 'abc' \\\n 'ö'\n", 64 | pyformat.format_code( 65 | 'x = "abc" \\\n"ö"\n')) 66 | 67 | def test_format_code_with_remove_all_unused_imports(self): 68 | self.assertEqual( 69 | "x = 'abc' \\\n 'ö'\n", 70 | pyformat.format_code( 71 | 'import my_module\nx = "abc" \\\n"ö"\n', 72 | aggressive=True, 73 | remove_all_unused_imports=True)) 74 | 75 | def test_format_code_with_remove_unused_variables(self): 76 | self.assertEqual( 77 | 'def test():\n return 42\n', 78 | pyformat.format_code( 79 | 'def test():\n x = 4\n return 42', 80 | aggressive=True, 81 | remove_unused_variables=True)) 82 | 83 | def test_format_multiple_files(self): 84 | with temporary_file('''\ 85 | if True: 86 | x = "abc" 87 | ''') as filename: 88 | output_file = io.StringIO() 89 | 90 | result = pyformat.format_multiple_files( 91 | [filename], 92 | pyformat.parse_args(['my_fake_program', '--in-place', '']), 93 | standard_out=output_file, 94 | standard_error=None) 95 | 96 | self.assertTrue(result) 97 | 98 | with open(filename) as f: 99 | self.assertEqual('''\ 100 | if True: 101 | x = 'abc' 102 | ''', f.read()) 103 | 104 | def test_format_multiple_files_should_return_false_on_no_change(self): 105 | with temporary_file('''\ 106 | if True: 107 | x = 'abc' 108 | ''') as filename: 109 | output_file = io.StringIO() 110 | 111 | result = pyformat.format_multiple_files( 112 | [filename], 113 | pyformat.parse_args(['my_fake_program', '--in-place', '']), 114 | standard_out=output_file, 115 | standard_error=None) 116 | 117 | self.assertFalse(result[0]) 118 | 119 | def test_format_multiple_files_with_nonexistent_file(self): 120 | output_file = io.StringIO() 121 | 122 | result = pyformat.format_multiple_files( 123 | ['nonexistent_file'], 124 | pyformat.parse_args(['my_fake_program', '--in-place', '']), 125 | standard_out=output_file, 126 | standard_error=output_file) 127 | 128 | self.assertFalse(result[0]) 129 | self.assertTrue(result[1]) 130 | 131 | def test_format_multiple_files_with_nonexistent_file_and_verbose(self): 132 | output_file = io.StringIO() 133 | 134 | result = pyformat.format_multiple_files( 135 | ['nonexistent_file'], 136 | pyformat.parse_args(['my_fake_program', 137 | '--in-place', '--verbose', '']), 138 | standard_out=output_file, 139 | standard_error=output_file) 140 | 141 | self.assertFalse(result[0]) 142 | self.assertTrue(result[1]) 143 | 144 | 145 | class TestSystem(unittest.TestCase): 146 | 147 | def test_diff(self): 148 | with temporary_file('''\ 149 | import os 150 | x = "abc" 151 | ''') as filename: 152 | output_file = io.StringIO() 153 | pyformat._main(argv=['my_fake_program', filename], 154 | standard_out=output_file, 155 | standard_error=None) 156 | self.assertEqual('''\ 157 | @@ -1,2 +1,2 @@ 158 | import os 159 | -x = "abc" 160 | +x = 'abc' 161 | ''', '\n'.join(output_file.getvalue().split('\n')[2:])) 162 | 163 | def test_diff_with_aggressive(self): 164 | with temporary_file('''\ 165 | import os 166 | x = "abc" 167 | ''') as filename: 168 | output_file = io.StringIO() 169 | pyformat._main(argv=['my_fake_program', '--aggressive', filename], 170 | standard_out=output_file, 171 | standard_error=None) 172 | self.assertEqual('''\ 173 | @@ -1,2 +1 @@ 174 | -import os 175 | -x = "abc" 176 | +x = 'abc' 177 | ''', '\n'.join(output_file.getvalue().split('\n')[2:])) 178 | 179 | def test_diff_with_empty_file(self): 180 | with temporary_file('') as filename: 181 | output_file = io.StringIO() 182 | pyformat._main(argv=['my_fake_program', filename], 183 | standard_out=output_file, 184 | standard_error=None) 185 | self.assertEqual('', output_file.getvalue()) 186 | 187 | def test_diff_with_encoding_declaration(self): 188 | with temporary_file("""\ 189 | # coding: utf-8 190 | import re 191 | import os 192 | import my_own_module 193 | x = 1 194 | """) as filename: 195 | output_file = io.StringIO() 196 | pyformat._main(argv=['my_fake_program', '--aggressive', filename], 197 | standard_out=output_file, 198 | standard_error=None) 199 | self.assertEqual("""\ 200 | # coding: utf-8 201 | -import re 202 | -import os 203 | import my_own_module 204 | x = 1 205 | """, '\n'.join(output_file.getvalue().split('\n')[3:])) 206 | 207 | def test_diff_with_nonexistent_file(self): 208 | output_file = io.StringIO() 209 | pyformat._main(argv=['my_fake_program', 'nonexistent_file'], 210 | standard_out=output_file, 211 | standard_error=output_file) 212 | self.assertIn('no such file', output_file.getvalue().lower()) 213 | 214 | def test_verbose(self): 215 | output_file = io.StringIO() 216 | pyformat._main(argv=['my_fake_program', '--verbose', __file__], 217 | standard_out=output_file, 218 | standard_error=output_file) 219 | self.assertIn('.py', output_file.getvalue()) 220 | 221 | def test_in_place(self): 222 | with temporary_file('''\ 223 | if True: 224 | x = "abc" 225 | ''') as filename: 226 | output_file = io.StringIO() 227 | pyformat._main(argv=['my_fake_program', '--in-place', filename], 228 | standard_out=output_file, 229 | standard_error=None) 230 | with open(filename) as f: 231 | self.assertEqual('''\ 232 | if True: 233 | x = 'abc' 234 | ''', f.read()) 235 | 236 | def test_multiple_jobs(self): 237 | with temporary_file('''\ 238 | if True: 239 | x = "abc" 240 | ''') as filename: 241 | output_file = io.StringIO() 242 | pyformat._main(argv=['my_fake_program', '--in-place', 243 | '--jobs=2', filename], 244 | standard_out=output_file, 245 | standard_error=None) 246 | with open(filename) as f: 247 | self.assertEqual('''\ 248 | if True: 249 | x = 'abc' 250 | ''', f.read()) 251 | 252 | def test_multiple_jobs_should_require_in_place(self): 253 | output_file = io.StringIO() 254 | self.assertEqual( 255 | 2, 256 | pyformat._main(argv=['my_fake_program', 257 | '--jobs=2', __file__], 258 | standard_out=output_file, 259 | standard_error=output_file)) 260 | 261 | self.assertIn('requires --in-place', output_file.getvalue()) 262 | 263 | def test_jobs_less_than_one_should_default_to_cpu_count(self): 264 | args = pyformat.parse_args(['my_fake_program', 265 | '--jobs=0', __file__]) 266 | 267 | self.assertGreater(args.jobs, 0) 268 | 269 | def test_remove_all_unused_imports_requires_aggressive(self): 270 | output_file = io.StringIO() 271 | self.assertEqual( 272 | 2, 273 | pyformat._main(argv=['my_fake_program', 274 | '--remove-all-unused-imports', __file__], 275 | standard_out=output_file, 276 | standard_error=output_file)) 277 | 278 | self.assertIn('requires --aggressive', output_file.getvalue()) 279 | 280 | def test_remove_unused_variables_requires_aggressive(self): 281 | output_file = io.StringIO() 282 | self.assertEqual( 283 | 2, 284 | pyformat._main( 285 | argv=['my_fake_program', 286 | '--remove-unused-variables', __file__], 287 | standard_out=output_file, 288 | standard_error=output_file)) 289 | 290 | self.assertIn('requires --aggressive', output_file.getvalue()) 291 | 292 | def test_ignore_hidden_directories(self): 293 | with temporary_directory() as directory: 294 | with temporary_directory(prefix='.', 295 | directory=directory) as inner_directory: 296 | 297 | with temporary_file("""\ 298 | if True: 299 | x = "abc" 300 | """, directory=inner_directory): 301 | 302 | output_file = io.StringIO() 303 | pyformat._main(argv=['my_fake_program', 304 | '--recursive', 305 | directory], 306 | standard_out=output_file, 307 | standard_error=None) 308 | self.assertEqual( 309 | '', 310 | output_file.getvalue().strip()) 311 | 312 | def test_recursive(self): 313 | with temporary_directory() as directory: 314 | with temporary_file("""\ 315 | if True: 316 | x = "abc" 317 | """, prefix='food', directory=directory): 318 | 319 | output_file = io.StringIO() 320 | pyformat._main(argv=['my_fake_program', 321 | '--recursive', 322 | '--exclude=zap', 323 | '--exclude=x*oo*', 324 | directory], 325 | standard_out=output_file, 326 | standard_error=None) 327 | self.assertEqual("""\ 328 | @@ -1,2 +1,2 @@ 329 | if True: 330 | - x = "abc" 331 | + x = 'abc' 332 | """, '\n'.join(output_file.getvalue().split('\n')[2:])) 333 | 334 | def test_exclude(self): 335 | with temporary_directory() as directory: 336 | with temporary_file("""\ 337 | if True: 338 | x = "abc" 339 | """, prefix='food', directory=directory): 340 | 341 | output_file = io.StringIO() 342 | pyformat._main(argv=['my_fake_program', 343 | '--recursive', 344 | '--exclude=zap', 345 | '--exclude=*oo*', 346 | directory], 347 | standard_out=output_file, 348 | standard_error=None) 349 | self.assertEqual( 350 | '', 351 | output_file.getvalue().strip()) 352 | 353 | def test_remove_all_unused_imports(self): 354 | with temporary_file("""\ 355 | import my_module 356 | 357 | def test(): 358 | return 42 359 | """) as filename: 360 | output_file = io.StringIO() 361 | pyformat._main(argv=['my_fake_program', 362 | '--in-place', 363 | '--aggressive', 364 | '--remove-all-unused-imports', 365 | filename], 366 | standard_out=output_file, 367 | standard_error=None) 368 | with open(filename) as f: 369 | self.assertEqual('''\ 370 | 371 | def test(): 372 | return 42 373 | ''', f.read()) 374 | 375 | def test_remove_unused_variables(self): 376 | with temporary_file("""\ 377 | def test(): 378 | x = 43 379 | return 42 380 | """) as filename: 381 | output_file = io.StringIO() 382 | pyformat._main(argv=['my_fake_program', 383 | '--in-place', 384 | '--aggressive', 385 | '--remove-unused-variables', 386 | filename], 387 | standard_out=output_file, 388 | standard_error=None) 389 | with open(filename) as f: 390 | self.assertEqual('''\ 391 | def test(): 392 | return 42 393 | ''', f.read()) 394 | 395 | def test_end_to_end(self): 396 | with temporary_file("""\ 397 | import os 398 | x = "abc" 399 | """) as filename: 400 | output = subprocess.check_output(PYFORMAT_COMMAND + [filename]) 401 | self.assertEqual("""\ 402 | import os 403 | -x = "abc" 404 | +x = 'abc' 405 | """, '\n'.join(output.decode().split('\n')[3:])) 406 | 407 | def test_no_config(self): 408 | source = """\ 409 | x =1 410 | """ 411 | expected = '' 412 | expected_no_config = """\ 413 | @@ -1 +1 @@ 414 | -x =1 415 | +x = 1 416 | """ 417 | setup_cfg = """\ 418 | [pep8] 419 | ignore=E 420 | """ 421 | with temporary_directory() as directory: 422 | with temporary_file(source, directory=directory) as filename: 423 | 424 | with open(os.path.join(directory, 'setup.cfg'), 425 | 'w') as setup_file: 426 | setup_file.write(setup_cfg) 427 | 428 | output_file = io.StringIO() 429 | pyformat._main(argv=['my_fake_program', 430 | '--aggressive', 431 | filename], 432 | standard_out=output_file, 433 | standard_error=None) 434 | self.assertEqual(expected, '\n'.join( 435 | output_file.getvalue().split('\n')[2:])) 436 | 437 | output_file = io.StringIO() 438 | pyformat._main(argv=['my_fake_program', 439 | '--aggressive', 440 | '--no-config', 441 | filename], 442 | standard_out=output_file, 443 | standard_error=None) 444 | self.assertEqual(expected_no_config, '\n'.join( 445 | output_file.getvalue().split('\n')[2:])) 446 | 447 | 448 | @contextlib.contextmanager 449 | def temporary_file(contents, directory='.', prefix=''): 450 | """Write contents to temporary file and yield it.""" 451 | f = tempfile.NamedTemporaryFile(suffix='.py', prefix=prefix, 452 | delete=False, dir=directory) 453 | try: 454 | f.write(contents.encode()) 455 | f.close() 456 | yield f.name 457 | finally: 458 | os.remove(f.name) 459 | 460 | 461 | @contextlib.contextmanager 462 | def temporary_directory(directory='.', prefix=''): 463 | """Create temporary directory and yield its path.""" 464 | temp_directory = tempfile.mkdtemp(prefix=prefix, dir=directory) 465 | try: 466 | yield temp_directory 467 | finally: 468 | shutil.rmtree(temp_directory) 469 | 470 | 471 | if __name__ == '__main__': 472 | unittest.main() 473 | --------------------------------------------------------------------------------