├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── .pylintrc ├── backports ├── __init__.py ├── LICENSE └── shutil_which.py ├── Context.sublime-menu ├── Default.sublime-commands ├── .gitignore ├── Main.sublime-menu ├── README.md ├── PyYapf.sublime-settings ├── LICENSE.md └── PyYapf.py /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+f"], "command": "yapf_selection" } 3 | ] 4 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+f"], "command": "yapf_selection" } 3 | ] 4 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+f"], "command": "yapf_selection" } 3 | ] 4 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=line-too-long,import-error,missing-docstring,too-few-public-methods,invalid-name,locally-disabled 3 | -------------------------------------------------------------------------------- /backports/__init__.py: -------------------------------------------------------------------------------- 1 | # from https://github.com/minrk/backports.shutil_which 2 | 3 | from pkgutil import extend_path 4 | __path__ = extend_path(__path__, __name__) -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "PyYapf: Format Selection", 4 | "command": "yapf_selection" 5 | }, 6 | { 7 | "caption": "PyYapf: Format Document", 8 | "command": "yapf_document" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "PyYapf: Format Selection", 4 | "command": "yapf_selection" 5 | }, 6 | { 7 | "caption": "PyYapf: Format Document", 8 | "command": "yapf_document" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mnemonic": "n", 4 | "caption": "Preferences", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "mnemonic": "P", 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "PyYapf", 14 | "children": [ 15 | { 16 | "caption": "README", 17 | "command": "open_file", 18 | "args": { 19 | "file": "${packages}/PyYapf Python Formatter/README.md" 20 | } 21 | }, 22 | { 23 | "caption": "Settings", 24 | "command": "edit_settings", 25 | "args": { 26 | "base_file": "${packages}/PyYapf Python Formatter/PyYapf.sublime-settings" 27 | } 28 | }, 29 | { 30 | "caption": "Key Bindings", 31 | "command": "edit_settings", 32 | "args": { 33 | "base_file": "${packages}/PyYapf Python Formatter/Default ($platform).sublime-keymap" 34 | } 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /backports/LICENSE: -------------------------------------------------------------------------------- 1 | This repo is Copyright (c) 2016 Min RK, and licensed under the MIT license. 2 | 3 | Since the source of `shutil.which` is a backport from a Python standard library module, 4 | the code itself is licensed under the Python Software Foundation (PSF) License. 5 | The backporting part (setup.py, etc.) are MIT. 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2016 Min RK 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyYapf 2 | 3 | Sublime Text 2-3 plugin to run the [YAPF](https://github.com/google/yapf) Python formatter 4 | 5 | ## Usage 6 | 7 | By default, press `Ctrl-Alt-F` to format the current selection (or the entire document if nothing is selected). 8 | You can also `Ctrl-Shift-P` (Mac: `Cmd-Shift-P`) and select "PyYapf: Format Selection" or "PyYapf: Format Document". 9 | To automatically run YAPF on the current document before saving, use the `on_save` setting. 10 | 11 | ## Installation 12 | 13 | 1. Install YAPF (if you haven't already): 14 | ``` 15 | pip install yapf 16 | ``` 17 | 18 | 2. Install Sublime Package Control by following the instructions [here](https://packagecontrol.io/installation) (if you haven't already). 19 | 20 | 3. `Ctrl-Shift-P` (Mac: `Cmd-Shift-P`) and choose "Package Control: Install Package". 21 | 22 | 4. Find "PyYapf Python Formatter" in the list (type in a few characters and you should see it). 23 | 24 | Alternatively, install manually by navigating to Sublime's `Packages` folder and cloning this repository: 25 | 26 | git clone https://github.com/jason-kane/PyYapf.git "PyYapf Python Formatter" 27 | 28 | ## Problems? 29 | 30 | This makes it pretty easy to find valid python code that makes Yapf choke or give bad formatting. 31 | Please try to reduce any problems to a minimal example and [let the YAPF folks know](https://github.com/google/yapf/issues). 32 | If there is something wrong with this plugin, [add an issue](https://github.com/jason-kane/PyYapf/issues) on GitHub and I'll try to address it. 33 | 34 | ## Distribution 35 | 36 | [Package Control](https://packagecontrol.io/packages/PyYapf%20Python%20Formatter) 37 | 38 | ## LICENSE 39 | 40 | Apache v2 per LICENSE. Do what you want; if you fix something please share it. 41 | -------------------------------------------------------------------------------- /backports/shutil_which.py: -------------------------------------------------------------------------------- 1 | """Backport of shutil.which from Python 3.5 2 | 3 | The function is included unmodified from Python stdlib 3.5.1, 4 | and is (C) Python 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | __version__ = '3.5.1' 11 | 12 | def backport_which(cmd, mode=os.F_OK | os.X_OK, path=None): 13 | """Given a command, mode, and a PATH string, return the path which 14 | conforms to the given mode on the PATH, or None if there is no such 15 | file. 16 | 17 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 18 | of os.environ.get("PATH"), or can be overridden with a custom search 19 | path. 20 | 21 | """ 22 | # Check that a given file can be accessed with the correct mode. 23 | # Additionally check that `file` is not a directory, as on Windows 24 | # directories pass the os.access check. 25 | def _access_check(fn, mode): 26 | return (os.path.exists(fn) and os.access(fn, mode) 27 | and not os.path.isdir(fn)) 28 | 29 | # If we're given a path with a directory part, look it up directly rather 30 | # than referring to PATH directories. This includes checking relative to the 31 | # current directory, e.g. ./script 32 | if os.path.dirname(cmd): 33 | if _access_check(cmd, mode): 34 | return cmd 35 | return None 36 | 37 | if path is None: 38 | path = os.environ.get("PATH", os.defpath) 39 | if not path: 40 | return None 41 | path = path.split(os.pathsep) 42 | 43 | if sys.platform == "win32": 44 | # The current directory takes precedence on Windows. 45 | if not os.curdir in path: 46 | path.insert(0, os.curdir) 47 | 48 | # PATHEXT is necessary to check on Windows. 49 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 50 | # See if the given file matches any of the expected path extensions. 51 | # This will allow us to short circuit when given "python.exe". 52 | # If it does match, only test that one, otherwise we have to try 53 | # others. 54 | if any(cmd.lower().endswith(ext.lower()) for ext in pathext): 55 | files = [cmd] 56 | else: 57 | files = [cmd + ext for ext in pathext] 58 | else: 59 | # On other platforms you don't have things like PATHEXT to tell you 60 | # what file suffixes are executable, so just pass on cmd as-is. 61 | files = [cmd] 62 | 63 | seen = set() 64 | for dir in path: 65 | normdir = os.path.normcase(dir) 66 | if not normdir in seen: 67 | seen.add(normdir) 68 | for thefile in files: 69 | name = os.path.join(dir, thefile) 70 | if _access_check(name, mode): 71 | return name 72 | return None 73 | 74 | try: 75 | from shutil import which 76 | except ImportError: 77 | which = backport_which -------------------------------------------------------------------------------- /PyYapf.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // full path and command to run yapf 3 | "yapf_command": "", 4 | 5 | // reformat entire file if no text is selected 6 | "use_entire_file_if_no_selection": true, 7 | 8 | // run yapf before saving document 9 | "on_save": false, 10 | 11 | // ignore files matching glob(s) 12 | "onsave_ignore_fn_glob": ["*.pyx"], 13 | 14 | // report errors in popup dialog (in addition to status bar) 15 | "popup_errors": false, 16 | 17 | // if no encoding is specified use this. utf-8 is a good choice, 18 | // ascii is (much) more restrictive. any of these should work: 19 | // https://docs.python.org/2/library/codecs.html#standard-encodings 20 | "default_encoding": "UTF-8", 21 | 22 | // custom yapf style options 23 | // 24 | // if commented out then yapf will search for the formatting style in the following manner: 25 | // 1. in the [style] section of a .style.yapf file in either the current directory or one of its parent directories. 26 | // 2. in the [yapf] section of a setup.cfg file in either the current directory or one of its parent directories. 27 | // 3. in the ~/.config/yapf/style file in your home directory. 28 | // if none of those files are found, the default style is used (PEP8). 29 | // 30 | /* 31 | "config": { 32 | // Determines which of the predefined styles this custom style is based on. 33 | "based_on_style": "pep8", 34 | 35 | // Align closing bracket with visual indentation. 36 | "ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT": true, 37 | 38 | // Insert a blank line before a 'def' or 'class' immediately 39 | // nested within another 'def' or 'class'. 40 | // 41 | // For example: 42 | // 43 | // class Foo: 44 | // # <------ this blank line 45 | // def method(): 46 | // ... 47 | // 48 | "BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF": false, 49 | 50 | // The column limit. 51 | "COLUMN_LIMIT": 79, 52 | 53 | // Indent width for line continuations. 54 | "CONTINUATION_INDENT_WIDTH": 4, 55 | 56 | // Put closing brackets on a separate line, dedented, if the 57 | // bracketed expression can't fit in a single line. Applies to 58 | // all kinds of brackets, including function definitions and calls. 59 | // 60 | // For example: 61 | // 62 | // config = { 63 | // 'key1': 'value1', 64 | // 'key2': 'value2', 65 | // } # <--- this bracket is dedented and on a separate line 66 | // 67 | // time_series = self.remote_client.query_entity_counters( 68 | // entity='dev3246.region1', 69 | // key='dns.query_latency_tcp', 70 | // transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 71 | // start_ts=now()-timedelta(days=3), 72 | // end_ts=now(), 73 | // ) # <--- this bracket is dedented and on a separate line 74 | 75 | "DEDENT_CLOSING_BRACKETS": false, 76 | 77 | // The regex for an i18n comment. The presence of this comment stops 78 | // reformatting of that line, because the comments are required to be 79 | // next to the string they translate. 80 | "I18N_COMMENT": "", 81 | 82 | // The i18n function call names. The presence of this function stops 83 | // reformattting on that line, because the string it has cannot be moved 84 | // away from the i18n comment. 85 | "I18N_FUNCTION_CALL": "", 86 | 87 | // Indent the dictionary value if it cannot fit on the same line as the dictionary key. 88 | // 89 | // For example: 90 | // 91 | // config = { 92 | // 'key1': 93 | // 'value1', 94 | // 'key2': value1 + 95 | // value2, 96 | // } 97 | "INDENT_DICTIONARY_VALUE": false, 98 | 99 | // The number of columns to use for indentation. 100 | "INDENT_WIDTH": 4, 101 | 102 | // Join short lines into one line. E.g., single line if statements. 103 | "JOIN_MULTIPLE_LINES": true, 104 | 105 | // Insert a space between the ending comma and closing bracket of a list, 106 | // etc. 107 | "SPACE_BETWEEN_ENDING_COMMA_AND_CLOSING_BRACKET": true, 108 | 109 | // The number of spaces required before a trailing comment. 110 | "SPACES_BEFORE_COMMENT": 2, 111 | 112 | // Set to True to prefer splitting before &, | or ^ rather than after. 113 | "SPLIT_BEFORE_BITWISE_OPERATOR": true, 114 | 115 | // Set to True to prefer splitting before 'and' or 'or' rather than 116 | // after. 117 | "SPLIT_BEFORE_LOGICAL_OPERATOR": false, 118 | 119 | // Split named assignments onto individual lines. 120 | "SPLIT_BEFORE_NAMED_ASSIGNS": true, 121 | 122 | // The penalty for splitting right after the opening bracket. 123 | "SPLIT_PENALTY_AFTER_OPENING_BRACKET": 30, 124 | 125 | // The penalty for splitting the line after a unary operator. 126 | "SPLIT_PENALTY_AFTER_UNARY_OPERATOR": 10000, 127 | 128 | // The penalty of splitting the line around the &, |, and ^ operators. 129 | "SPLIT_PENALTY_BITWISE_OPERATOR": 300, 130 | 131 | // The penalty for characters over the column limit. 132 | "SPLIT_PENALTY_EXCESS_CHARACTER": 2500, 133 | 134 | // The penalty incurred by adding a line split to the unwrapped line. The 135 | // more line splits added the higher the penalty. 136 | "SPLIT_PENALTY_FOR_ADDED_LINE_SPLIT": 30, 137 | 138 | // The penalty of splitting a list of import as names. 139 | // 140 | // For example: 141 | // 142 | // from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 143 | // long_argument_2, 144 | // long_argument_3) 145 | // 146 | // would reformat to something like: 147 | // 148 | // from a_very_long_or_indented_module_name_yada_yad import ( 149 | // long_argument_1, long_argument_2, long_argument_3) 150 | "SPLIT_PENALTY_IMPORT_NAMES": 0, 151 | 152 | // The penalty of splitting the line around the 'and' and 'or' operators. 153 | "SPLIT_PENALTY_LOGICAL_OPERATOR": 300, 154 | }, 155 | */ 156 | 157 | // pass source code on stdin (otherwise, a temporary file is used). only 158 | // when this option is enabled yapf will pick up the style as described 159 | // in yapf --help. disable this option in case of UnicodeEncodeErrors 160 | // (see https://github.com/jason-kane/PyYapf/issues/36, as well as 161 | // https://github.com/jason-kane/PyYapf/issues/20 and the related 162 | // upstream bug https://github.com/google/yapf/pull/145) 163 | "use_stdin": true, 164 | 165 | // add extra output to the console for debugging pyyapf/yapf behavior 166 | "debug": false 167 | } 168 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PyYapf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Sublime Text 2-3 Plugin to invoke YAPF on a Python file. 4 | """ 5 | from __future__ import print_function 6 | try: 7 | import configparser 8 | except ImportError: 9 | import ConfigParser as configparser 10 | 11 | import fnmatch 12 | import os 13 | import shlex 14 | import subprocess 15 | import sys 16 | import tempfile 17 | import textwrap 18 | 19 | import sublime 20 | import sublime_plugin 21 | 22 | try: 23 | from shutil import which 24 | except ImportError: 25 | from backports.shutil_which import which 26 | 27 | # make sure we don't choke on unicode when we reformat ourselves 28 | u"我爱蟒蛇" 29 | 30 | SUBLIME_3 = sys.version_info >= (3, 0) 31 | KEY = "pyyapf" 32 | 33 | PLUGIN_SETTINGS_FILE = "PyYapf.sublime-settings" 34 | SUBLIME_SETTINGS_KEY = "PyYapf" 35 | 36 | if not SUBLIME_3: 37 | # backport from python 3.3 38 | # (https://hg.python.org/cpython/file/3.3/Lib/textwrap.py) 39 | def indent(text, prefix, predicate=None): 40 | """Add 'prefix' to the beginning of selected lines in 'text'. 41 | 42 | If 'predicate' is provided, 'prefix' will only be added to the lines 43 | where 'predicate(line)' is True. If 'predicate' is not provided, 44 | it will default to adding 'prefix' to all non-empty lines that do not 45 | consist solely of whitespace characters. 46 | """ 47 | if predicate is None: 48 | 49 | def predicate(line): 50 | return line.strip() 51 | 52 | def prefixed_lines(): 53 | for line in text.splitlines(True): 54 | yield (prefix + line if predicate(line) else line) 55 | 56 | return ''.join(prefixed_lines()) 57 | 58 | textwrap.indent = indent 59 | 60 | 61 | def save_style_to_tempfile(style): 62 | """Build yapf style config file.""" 63 | cfg = configparser.RawConfigParser() 64 | cfg.add_section('style') 65 | for key, value in style.items(): 66 | cfg.set('style', key, value) 67 | 68 | # dump it to temporary file 69 | fobj, fname = tempfile.mkstemp() 70 | cfg.write(os.fdopen(fobj, "w")) 71 | return fname 72 | 73 | 74 | def dedent_text(text): 75 | """Strip initial whitespace from text but note how wide it is.""" 76 | new_text = textwrap.dedent(text) 77 | if not new_text: 78 | return new_text, '', False 79 | 80 | # determine original indentation 81 | old_first = text.splitlines()[0] 82 | new_first = new_text.splitlines()[0] 83 | assert old_first.endswith(new_first), 'PyYapf: Dedent logic flawed' 84 | indent = old_first[:len(old_first) - len(new_first)] 85 | 86 | # determine if have trailing newline (when using the "yapf_selection" 87 | # command, it can happen that there is none) 88 | trailing_nl = text.endswith('\n') 89 | 90 | return new_text, indent, trailing_nl 91 | 92 | 93 | def indent_text(text, indent, trailing_nl): 94 | """Reindent text by `indent` characters.""" 95 | text = textwrap.indent(text, indent) 96 | 97 | # remove trailing newline if so desired 98 | if not trailing_nl and text.endswith('\n'): 99 | text = text[:-1] 100 | 101 | return text 102 | 103 | 104 | def parse_error_line(err_lines): 105 | """Parse YAPF output to determine line on which error occurred.""" 106 | msg = err_lines[-1] 107 | 108 | # yapf.yapflib.verifier.InternalError: 109 | # Missing parentheses in call to 'print' (, line 2) 110 | if '(, line ' in msg: 111 | return int(msg.rstrip(')').rsplit(None, 1)[1]) + 1 112 | 113 | # lib2to3.pgen2.tokenize.TokenError: ('EOF in multi-line statement', (5, 0)) 114 | if msg.endswith('))'): 115 | return int(msg.rstrip(')').rsplit(None, 2)[1].strip(',(')) 116 | 117 | # File "", line 3 118 | # if: 119 | # ^ 120 | # SyntaxError: invalid syntax 121 | if len(err_lines) >= 4 and ', line' in err_lines[-4]: 122 | return int(err_lines[-4].rsplit(None, 1)[1]) 123 | 124 | 125 | if SUBLIME_3: 126 | ERROR_FLAGS = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE 127 | else: 128 | ERROR_FLAGS = sublime.DRAW_OUTLINED 129 | 130 | 131 | class Yapf: 132 | """ 133 | This class wraps YAPF invocation. 134 | 135 | Includes encoding/decoding and error handling. 136 | """ 137 | 138 | def __init__(self, view): 139 | """We are tied to a specific view (an open file in sublime).""" 140 | self.view = view 141 | 142 | def __enter__(self): 143 | """Sublime calls plugins 'with' a context manager.""" 144 | # determine encoding 145 | self.encoding = self.view.encoding() 146 | if self.encoding in ['Undefined', None]: 147 | self.encoding = self.get_setting('default_encoding') 148 | self.debug( 149 | 'Encoding is not specified, falling back to default %r', 150 | self.encoding 151 | ) 152 | else: 153 | self.debug('Encoding is %r', self.encoding) 154 | 155 | # custom style options? 156 | custom_style = self.get_setting("config") 157 | if custom_style: 158 | # write style file to temporary file 159 | self.custom_style_fname = save_style_to_tempfile(custom_style) 160 | self.debug( 161 | 'Using custom style (%s):\n%s', self.custom_style_fname, 162 | open(self.custom_style_fname).read().strip() 163 | ) 164 | else: 165 | self.custom_style_fname = None 166 | 167 | # use shlex.split because we should honor embedded quoted arguemnts 168 | self.popen_args = shlex.split(self.find_yapf(), posix=False) 169 | if self.custom_style_fname: 170 | self.popen_args += ['--style', self.custom_style_fname] 171 | 172 | # use directory of current file so that custom styles are found 173 | # properly 174 | fname = self.view.file_name() 175 | self.popen_cwd = os.path.dirname(fname) if fname else None 176 | 177 | # specify encoding in environment 178 | self.popen_env = os.environ.copy() 179 | self.popen_env['LANG'] = str(self.encoding) 180 | 181 | # win32: hide console window 182 | if sys.platform in ('win32', 'cygwin'): 183 | self.popen_startupinfo = subprocess.STARTUPINFO() 184 | self.popen_startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW 185 | self.popen_startupinfo.wShowWindow = subprocess.SW_HIDE 186 | else: 187 | self.popen_startupinfo = None 188 | 189 | # clear marked regions and status 190 | self.view.erase_regions(KEY) 191 | self.view.erase_status(KEY) 192 | self.errors = [] 193 | return self 194 | 195 | def __exit__(self, type, value, traceback): 196 | """Context manager cleanup.""" 197 | if self.custom_style_fname: 198 | os.unlink(self.custom_style_fname) 199 | 200 | def find_yapf(self): 201 | """Find the yapf executable.""" 202 | # default to what is in the settings file 203 | cmd = self.get_setting("yapf_command") 204 | cmd = os.path.expanduser(cmd) 205 | 206 | # sublime 2.x support per https://github.com/jason-kane/PyYapf/issues/53 207 | if hasattr(sublime, "expand_variables"): 208 | cmd = sublime.expand_variables( 209 | cmd, 210 | sublime.active_window().extract_variables() 211 | ) 212 | 213 | save_settings = not cmd 214 | 215 | for maybe_cmd in ['yapf', 'yapf3', 'yapf.exe', 'yapf3.exe']: 216 | if not cmd: 217 | cmd = which(maybe_cmd) 218 | if cmd: 219 | self.debug('Found yapf: %s', cmd) 220 | break 221 | 222 | if cmd and save_settings: 223 | settings = sublime.load_settings(PLUGIN_SETTINGS_FILE) 224 | settings.set("yapf_command", cmd) 225 | sublime.save_settings(PLUGIN_SETTINGS_FILE) 226 | 227 | return cmd 228 | 229 | def format(self, edit, selection=None): 230 | """ 231 | Format selection (if None then formats the entire document). 232 | 233 | Returns region containing the reformatted text. 234 | """ 235 | # determine selection to format 236 | if not selection: 237 | selection = sublime.Region(0, self.view.size()) 238 | self.debug('Formatting selection %r', selection) 239 | 240 | # retrieve selected text & dedent 241 | text = self.view.substr(selection) 242 | text, indent, trailing_nl = dedent_text(text) 243 | self.debug('Detected indent %r', indent) 244 | 245 | # encode text 246 | try: 247 | encoded_text = text.encode(self.encoding) 248 | except UnicodeEncodeError as err: 249 | msg = ( 250 | "You may need to re-open this file with a different encoding." 251 | " Current encoding is %r." % self.encoding 252 | ) 253 | self.error("UnicodeEncodeError: %s\n\n%s", err, msg) 254 | return 255 | 256 | # pass source code to be formatted on stdin? 257 | if self.get_setting("use_stdin"): 258 | # run yapf 259 | self.debug('Running %s in %s', self.popen_args, self.popen_cwd) 260 | try: 261 | popen = subprocess.Popen( 262 | self.popen_args, 263 | stdout=subprocess.PIPE, 264 | stderr=subprocess.PIPE, 265 | stdin=subprocess.PIPE, 266 | cwd=self.popen_cwd, 267 | env=self.popen_env, 268 | startupinfo=self.popen_startupinfo 269 | ) 270 | except OSError as err: 271 | # always show error in popup 272 | msg = "You may need to install YAPF and/or configure 'yapf_command' in PyYapf's Settings." 273 | sublime.error_message("OSError: %s\n\n%s" % (err, msg)) 274 | return 275 | encoded_stdout, encoded_stderr = popen.communicate(encoded_text) 276 | text = encoded_stdout.decode(self.encoding) 277 | else: 278 | # do _not_ use stdin. this avoids a unicode defect in yapf, see 279 | # https://github.com/google/yapf/pull/145. the downside is that 280 | # .style.yapf / setup.cfg configuration is not picked up properly, 281 | # see https://github.com/jason-kane/PyYapf/issues/36. 282 | # we may permanently use stdin and remove the use_stdin option and 283 | # this code once the upstream bug is fixed. 284 | file_obj, temp_filename = tempfile.mkstemp(suffix=".py") 285 | try: 286 | temp_handle = os.fdopen(file_obj, 'wb' if SUBLIME_3 else 'w') 287 | temp_handle.write(encoded_text) 288 | temp_handle.close() 289 | self.popen_args += ["--in-place", temp_filename] 290 | 291 | self.debug('Running %s in %s', self.popen_args, self.popen_cwd) 292 | try: 293 | popen = subprocess.Popen( 294 | self.popen_args, 295 | stdout=subprocess.PIPE, 296 | stderr=subprocess.PIPE, 297 | cwd=self.popen_cwd, 298 | env=self.popen_env, 299 | startupinfo=self.popen_startupinfo 300 | ) 301 | except OSError as err: 302 | # always show error in popup 303 | msg = "You may need to install YAPF and/or configure 'yapf_command' in PyYapf's Settings." 304 | sublime.error_message("OSError: %s\n\n%s" % (err, msg)) 305 | return 306 | 307 | encoded_stdout, encoded_stderr = popen.communicate() 308 | 309 | if SUBLIME_3: 310 | open_encoded = open 311 | else: 312 | import codecs 313 | open_encoded = codecs.open 314 | 315 | with open_encoded(temp_filename, encoding=self.encoding) as fp: 316 | text = fp.read() 317 | finally: 318 | os.unlink(temp_filename) 319 | 320 | self.debug('Exit code %d', popen.returncode) 321 | 322 | # handle errors (since yapf>=0.3, exit code 2 means changed, not error) 323 | if popen.returncode not in (0, 2): 324 | stderr = encoded_stderr.decode(self.encoding) 325 | stderr = stderr.replace(os.linesep, '\n') 326 | self.debug('Error:\n%s', stderr) 327 | 328 | # report error 329 | err_lines = stderr.splitlines() 330 | msg = err_lines[-1] 331 | self.error('%s', msg) 332 | 333 | # attempt to highlight line where error occurred 334 | rel_line = parse_error_line(err_lines) 335 | if rel_line: 336 | line = self.view.rowcol(selection.begin())[0] 337 | pt = self.view.text_point(line + rel_line - 1, 0) 338 | region = self.view.line(pt) 339 | self.view.add_regions(KEY, [region], KEY, 'cross', ERROR_FLAGS) 340 | return 341 | 342 | # adjust newlines (only necessary when use_stdin is True, since 343 | # [codecs.]open uses universal newlines by default) 344 | text = text.replace(os.linesep, '\n') 345 | 346 | # re-indent and replace text 347 | text = indent_text(text, indent, trailing_nl) 348 | self.view.replace(edit, selection, text) 349 | 350 | # return region containing modified text 351 | if selection.a <= selection.b: 352 | return sublime.Region(selection.a, selection.a + len(text)) 353 | else: 354 | return sublime.Region(selection.b + len(text), selection.b) 355 | 356 | def debug(self, msg, *args): 357 | """Logger that will be caught by sublimes ~ output screen.""" 358 | if self.get_setting('debug'): 359 | print('PyYapf:', msg % args) 360 | 361 | def error(self, msg, *args): 362 | """Logger to make errors as obvious as we can make them.""" 363 | msg = msg % args 364 | 365 | # add to status bar 366 | self.errors.append(msg) 367 | self.view.set_status(KEY, 'PyYapf: %s' % ', '.join(self.errors)) 368 | if self.get_setting('popup_errors'): 369 | sublime.error_message(msg) 370 | 371 | def get_setting(self, key, default_value=None): 372 | """Wrapper to return a single setting.""" 373 | return get_setting(self.view, key, default_value) 374 | 375 | 376 | def is_python(view): 377 | """Cosmetic sugar.""" 378 | return view.score_selector(0, 'source.python') > 0 379 | 380 | 381 | if not SUBLIME_3: 382 | 383 | class PreserveSelectionAndView: 384 | """ 385 | Context manager to preserve selection and view when text is replaced. 386 | 387 | Sublime Text 2 sucks at this, hence the manual lifting. 388 | """ 389 | 390 | def __init__(self, view): 391 | """Preserve the view (single open document).""" 392 | self.view = view 393 | 394 | def __enter__(self): 395 | """Save selection and view.""" 396 | self.sel = list(self.view.sel()) 397 | self.visible_region_begin = self.view.visible_region().begin() 398 | self.viewport_position = self.view.viewport_position() 399 | return self 400 | 401 | def __exit__(self, type, value, traceback): 402 | """Restore selection.""" 403 | self.view.sel().clear() 404 | for s in self.sel: 405 | self.view.sel().add(s) 406 | 407 | # restore view (this is somewhat cargo cultish, not sure why a 408 | # single statement does not suffice) 409 | self.view.show(self.visible_region_begin) 410 | self.view.set_viewport_position(self.viewport_position) 411 | else: 412 | 413 | class PreserveSelectionAndView: 414 | """ 415 | Context manager to preserve selection and view when text is replaced. 416 | 417 | Sublime Text 3 already does a good job preserving the view. 418 | """ 419 | 420 | def __init__(self, view): 421 | """Preserve view.""" 422 | self.view = view 423 | 424 | def __enter__(self): 425 | """Save selection.""" 426 | self.sel = list(self.view.sel()) 427 | return self 428 | 429 | def __exit__(self, type, value, traceback): 430 | """Restore selection.""" 431 | self.view.sel().clear() 432 | for s in self.sel: 433 | self.view.sel().add(s) 434 | 435 | 436 | class YapfSelectionCommand(sublime_plugin.TextCommand): 437 | """ 438 | The "yapf_selection" command formats the current selection. 439 | 440 | Will format the entire document if the "use_entire_file_if_no_selection" 441 | option is enabled. 442 | """ 443 | 444 | def is_enabled(self): 445 | """ 446 | Only allow yapf for python documents. 447 | 448 | I'm of two minds on this as I frequently use yapf to reformat 449 | json blobs. 450 | """ 451 | return is_python(self.view) 452 | 453 | def run(self, edit): 454 | """Sublime Text executes this when you trigger the TextCommand.""" 455 | with Yapf(self.view) as yapf: 456 | # no selection? 457 | no_selection = all(s.empty() for s in self.view.sel()) 458 | if no_selection: 459 | if not yapf.get_setting("use_entire_file_if_no_selection"): 460 | sublime.error_message('A selection is required') 461 | return 462 | 463 | # format entire document 464 | with PreserveSelectionAndView(self.view): 465 | yapf.format(edit) 466 | return 467 | 468 | # otherwise format all (non-empty) ones 469 | with PreserveSelectionAndView(self.view) as pv: 470 | pv.sel = [] 471 | for s in self.view.sel(): 472 | if not s.empty(): 473 | new_s = yapf.format(edit, s) 474 | pv.sel.append(new_s if new_s else s) 475 | 476 | 477 | class YapfDocumentCommand(sublime_plugin.TextCommand): 478 | """The "yapf_document" command formats the current document.""" 479 | 480 | def is_enabled(self): 481 | """ 482 | Only allow yapf for python documents. 483 | 484 | I'm of two minds on this as I frequently use yapf to reformat 485 | json blobs. 486 | """ 487 | return is_python(self.view) 488 | 489 | def run(self, edit): 490 | """Sublime Text executes this when you trigger the TextCommand.""" 491 | with PreserveSelectionAndView(self.view): 492 | with Yapf(self.view) as yapf: 493 | yapf.format(edit) 494 | 495 | 496 | class EventListener(sublime_plugin.EventListener): 497 | """Hook in to detect when a file is saved.""" 498 | 499 | def on_pre_save(self, view): # pylint: disable=no-self-use 500 | """Before we let ST save the file, run yapf on it.""" 501 | if get_setting(view, 'on_save'): 502 | if view.file_name() and get_setting(view, "onsave_ignore_fn_glob"): 503 | for pattern in get_setting(view, "onsave_ignore_fn_glob"): 504 | if fnmatch.fnmatch(view.file_name(), pattern): 505 | print( 506 | 'PyYapf: Skipping yapf, matches pattern {}'. 507 | format(pattern) 508 | ) 509 | return 510 | 511 | view.run_command('yapf_document') 512 | 513 | 514 | def get_setting(view, key, default_value=None): 515 | """Retrieve a key from the settings file.""" 516 | # 1. check sublime settings (this includes project settings) 517 | settings = sublime.active_window().active_view().settings() 518 | config = settings.get(SUBLIME_SETTINGS_KEY) 519 | if config is not None and key in config: 520 | return config[key] 521 | 522 | # 2. check plugin settings 523 | settings = sublime.load_settings(PLUGIN_SETTINGS_FILE) 524 | return settings.get(key, default_value) 525 | --------------------------------------------------------------------------------