├── .github ├── FUNDING.yml └── main.workflow ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── generate_commit_msg.py ├── reindent.py ├── upgrade_to_python3.py └── upgrade_to_python3_hack.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [cclauss] 2 | patreon: cclauss 3 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "New workflow" { 2 | on = "push" 3 | resolves = ["Find Python 3 syntax errors and undefined names"] 4 | } 5 | 6 | action "Find Python 3 syntax errors and undefined names" { 7 | secrets = ["GITHUB_TOKEN"] 8 | uses = "cclauss/Find-Python-syntax-errors-action@master" 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | LABEL "com.github.actions.name"="Upgrade to Python 3" 4 | LABEL "com.github.actions.description"="Create pull requests to upgrade your code to Python 3." 5 | LABEL "com.github.actions.icon"="upload-cloud" 6 | LABEL "com.github.actions.color"="6f42c1" 7 | 8 | COPY *.py / 9 | 10 | RUN apk update \ 11 | && apk upgrade \ 12 | && apk add --no-cache git openssh 13 | RUN pip install --upgrade pip 14 | RUN pip install flake8 future 15 | RUN python --version ; pip --version ; echo "flake8 $(flake8 --version)" ; echo "futurize $(futurize --version)" 16 | 17 | CMD ["python", "/upgrade_to_python3.py"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upgrade to Python3 2 | A GitHub Action that will upgrade your Python code to Python 3 3 | 4 | This action uses [__flake8__](http://flake8.pycqa.org) to know if your Python code has syntax errors. 5 | 6 | If syntax errors are found then this Action uses [__futurize__](http://python-future.org/futurize_cheatsheet.html) create pull requests that gradually upgrade that code to be more compatible with Python 3. After this Action has run, return to your repo and look for a "__modernize-Python-2-codes__" branch in your repo. If this branch exists then select it and __make pull request__ and make sure that your automate tests pass before merging the pull request. After merging, delete the "__modernize-Python-2-codes__" branch so that the process can be repeated. 7 | 8 | Example workflow (Put the following text into your repo's `.github/workflows/upgrade-to-py3.yml`): 9 | ``` 10 | on: 11 | push: 12 | branches: 13 | - master 14 | 15 | jobs: 16 | upgrade_to_Python3: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: "Upgrade to Python3" 21 | uses: cclauss/Upgrade-to-Python3@master 22 | - name: Commit files 23 | run: | 24 | git config --local user.email "me@me.me" 25 | git config --local user.name "GitHub Action" 26 | git commit -m "Add changes" -a 27 | - name: Push changes 28 | uses: peter-evans/create-pull-request@v2 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | branch: 'py3-addendum' 32 | ``` 33 | 34 | * https://github.com/marketplace/actions/upgrade-to-python-3 35 | * https://blog.jessfraz.com/post/the-life-of-a-github-action 36 | * https://developer.github.com/actions/creating-github-actions 37 | -------------------------------------------------------------------------------- /generate_commit_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from collections import namedtuple 4 | from typing import Iterable 5 | 6 | msg = namedtuple("msg", "head body") 7 | 8 | lookup = {"except": msg("Old style exceptions --> new style for Python 3", 9 | "Old style exceptions are syntax errors in Python 3 " 10 | "but new style exceptions work as expected in both Python " 11 | "2 and Python 3."), 12 | "exec": msg("Use __exec()__ function in both Python 2 and Python 3", 13 | "__exec()__ is a function in Python 3."), 14 | "print": msg("Use __print()__ function in both Python 2 and Python 3", 15 | "Legacy __print__ statements are syntax errors in Python 3 " 16 | "but __print()__ function works as expected in both Python " 17 | "2 and Python 3.")} 18 | 19 | 20 | def find_fixer(minus_text, plus_text): 21 | for func in ("exec", "print"): 22 | if (f"{func} " in minus_text and 23 | f"{func}(" not in minus_text and 24 | f"{func}(" in plus_text): 25 | return func 26 | if ("except " in minus_text and 27 | " as " not in minus_text and 28 | " as " in plus_text): 29 | return "except" 30 | 31 | 32 | def generate_body(fixers: Iterable[str]) -> str: 33 | messages = (lookup[fixer] for fixer in fixers) 34 | return "\n".join(f"* {msg.head}\n * {msg.body}" for msg in messages) 35 | 36 | 37 | def generate_commit_msg(diff_text): 38 | fixers = [] 39 | texts = {"-": "", "+": ""} 40 | for line in diff_text.splitlines(): 41 | current = line[0] if line else " " 42 | if current in "-+": 43 | texts[current] += line[1:] + " " 44 | elif texts["-"] or texts["+"]: 45 | print("\n".join(f"{key}: {value}"for key, value in texts.items())) 46 | fixer = find_fixer(texts["-"], texts["+"]) 47 | texts["-"] = texts["+"] = "" 48 | print(fixer) 49 | if fixer and fixer not in fixers: # fixers is like an ordered set 50 | fixers.append(fixer) 51 | if fixers == lookup.keys(): # we got them all! 52 | break 53 | 54 | if len(fixers) == 1: 55 | message = lookup[fixers[0]] 56 | title = message.head.replace("__", "") 57 | body = message.body 58 | else: 59 | title = "Modernize Python 2 code to get ready for Python 3" 60 | body = generate_body(fixers) 61 | return "\n\n".join((title, body)) 62 | 63 | 64 | if __name__ == "__main__": 65 | with open("tensorflow_models.diff") as in_file: 66 | print(generate_commit_msg(in_file.read())) 67 | -------------------------------------------------------------------------------- /reindent.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Released to the public domain, by Tim Peters, 03 October 2000. 4 | 5 | """reindent [-d][-r][-v] [ path ... ] 6 | 7 | -d (--dryrun) Dry run. Analyze, but don't make any changes to, files. 8 | -r (--recurse) Recurse. Search for all .py files in subdirectories too. 9 | -n (--nobackup) No backup. Does not make a ".bak" file before reindenting. 10 | -v (--verbose) Verbose. Print informative msgs; else no output. 11 | (--newline) Newline. Specify the newline character to use (CRLF, LF). 12 | Default is the same as the original file. 13 | -h (--help) Help. Print this usage information and exit. 14 | 15 | Change Python (.py) files to use 4-space indents and no hard tab characters. 16 | Also trim excess spaces and tabs from ends of lines, and remove empty lines 17 | at the end of files. Also ensure the last line ends with a newline. 18 | 19 | If no paths are given on the command line, reindent operates as a filter, 20 | reading a single source file from standard input and writing the transformed 21 | source to standard output. In this case, the -d, -r and -v flags are 22 | ignored. 23 | 24 | You can pass one or more file and/or directory paths. When a directory 25 | path, all .py files within the directory will be examined, and, if the -r 26 | option is given, likewise recursively for subdirectories. 27 | 28 | If output is not to standard output, reindent overwrites files in place, 29 | renaming the originals with a .bak extension. If it finds nothing to 30 | change, the file is left alone. If reindent does change a file, the changed 31 | file is a fixed-point for future runs (i.e., running reindent on the 32 | resulting .py file won't change it again). 33 | 34 | The hard part of reindenting is figuring out what to do with comment 35 | lines. So long as the input files get a clean bill of health from 36 | tabnanny.py, reindent should do a good job. 37 | 38 | The backup file is a copy of the one that is being reindented. The ".bak" 39 | file is generated with shutil.copy(), but some corner cases regarding 40 | user/group and permissions could leave the backup file more readable than 41 | you'd prefer. You can always use the --nobackup option to prevent this. 42 | """ 43 | 44 | __version__ = "1" 45 | 46 | import tokenize 47 | import os 48 | import shutil 49 | import sys 50 | 51 | verbose = False 52 | recurse = False 53 | dryrun = False 54 | makebackup = True 55 | # A specified newline to be used in the output (set by --newline option) 56 | spec_newline = None 57 | 58 | 59 | def usage(msg=None): 60 | if msg is None: 61 | msg = __doc__ 62 | print(msg, file=sys.stderr) 63 | 64 | 65 | def errprint(*args): 66 | sys.stderr.write(" ".join(str(arg) for arg in args)) 67 | sys.stderr.write("\n") 68 | 69 | def main(): 70 | import getopt 71 | global verbose, recurse, dryrun, makebackup, spec_newline 72 | try: 73 | opts, args = getopt.getopt(sys.argv[1:], "drnvh", 74 | ["dryrun", "recurse", "nobackup", "verbose", "newline=", "help"]) 75 | except getopt.error as msg: 76 | usage(msg) 77 | return 78 | for o, a in opts: 79 | if o in ('-d', '--dryrun'): 80 | dryrun = True 81 | elif o in ('-r', '--recurse'): 82 | recurse = True 83 | elif o in ('-n', '--nobackup'): 84 | makebackup = False 85 | elif o in ('-v', '--verbose'): 86 | verbose = True 87 | elif o in ('--newline',): 88 | if not a.upper() in ('CRLF', 'LF'): 89 | usage() 90 | return 91 | spec_newline = dict(CRLF='\r\n', LF='\n')[a.upper()] 92 | elif o in ('-h', '--help'): 93 | usage() 94 | return 95 | if not args: 96 | r = Reindenter(sys.stdin) 97 | r.run() 98 | r.write(sys.stdout) 99 | return 100 | for arg in args: 101 | check(arg) 102 | 103 | 104 | def check(file): 105 | if os.path.isdir(file) and not os.path.islink(file): 106 | if verbose: 107 | print("listing directory", file) 108 | names = os.listdir(file) 109 | for name in names: 110 | fullname = os.path.join(file, name) 111 | if ((recurse and os.path.isdir(fullname) and 112 | not os.path.islink(fullname) and 113 | not os.path.split(fullname)[1].startswith(".")) 114 | or name.lower().endswith(".py")): 115 | check(fullname) 116 | return 117 | 118 | if verbose: 119 | print("checking", file, "...", end=' ') 120 | with open(file, 'rb') as f: 121 | try: 122 | encoding, _ = tokenize.detect_encoding(f.readline) 123 | except SyntaxError as se: 124 | errprint("%s: SyntaxError: %s" % (file, str(se))) 125 | return 126 | try: 127 | with open(file, encoding=encoding) as f: 128 | r = Reindenter(f) 129 | except IOError as msg: 130 | errprint("%s: I/O Error: %s" % (file, str(msg))) 131 | return 132 | 133 | newline = spec_newline if spec_newline else r.newlines 134 | if isinstance(newline, tuple): 135 | errprint("%s: mixed newlines detected; cannot continue without --newline" % file) 136 | return 137 | 138 | if r.run(): 139 | if verbose: 140 | print("changed.") 141 | if dryrun: 142 | print("But this is a dry run, so leaving it alone.") 143 | if not dryrun: 144 | bak = file + ".bak" 145 | if makebackup: 146 | shutil.copyfile(file, bak) 147 | if verbose: 148 | print("backed up", file, "to", bak) 149 | with open(file, "w", encoding=encoding, newline=newline) as f: 150 | r.write(f) 151 | if verbose: 152 | print("wrote new", file) 153 | return True 154 | else: 155 | if verbose: 156 | print("unchanged.") 157 | return False 158 | 159 | 160 | def _rstrip(line, JUNK='\n \t'): 161 | """Return line stripped of trailing spaces, tabs, newlines. 162 | 163 | Note that line.rstrip() instead also strips sundry control characters, 164 | but at least one known Emacs user expects to keep junk like that, not 165 | mentioning Barry by name or anything . 166 | """ 167 | 168 | i = len(line) 169 | while i > 0 and line[i - 1] in JUNK: 170 | i -= 1 171 | return line[:i] 172 | 173 | 174 | class Reindenter: 175 | 176 | def __init__(self, f): 177 | self.find_stmt = 1 # next token begins a fresh stmt? 178 | self.level = 0 # current indent level 179 | 180 | # Raw file lines. 181 | self.raw = f.readlines() 182 | 183 | # File lines, rstripped & tab-expanded. Dummy at start is so 184 | # that we can use tokenize's 1-based line numbering easily. 185 | # Note that a line is all-blank iff it's "\n". 186 | self.lines = [_rstrip(line).expandtabs() + "\n" 187 | for line in self.raw] 188 | self.lines.insert(0, None) 189 | self.index = 1 # index into self.lines of next line 190 | 191 | # List of (lineno, indentlevel) pairs, one for each stmt and 192 | # comment line. indentlevel is -1 for comment lines, as a 193 | # signal that tokenize doesn't know what to do about them; 194 | # indeed, they're our headache! 195 | self.stats = [] 196 | 197 | # Save the newlines found in the file so they can be used to 198 | # create output without mutating the newlines. 199 | self.newlines = f.newlines 200 | 201 | def run(self): 202 | tokens = tokenize.generate_tokens(self.getline) 203 | for _token in tokens: 204 | self.tokeneater(*_token) 205 | # Remove trailing empty lines. 206 | lines = self.lines 207 | while lines and lines[-1] == "\n": 208 | lines.pop() 209 | # Sentinel. 210 | stats = self.stats 211 | stats.append((len(lines), 0)) 212 | # Map count of leading spaces to # we want. 213 | have2want = {} 214 | # Program after transformation. 215 | after = self.after = [] 216 | # Copy over initial empty lines -- there's nothing to do until 217 | # we see a line with *something* on it. 218 | i = stats[0][0] 219 | after.extend(lines[1:i]) 220 | for i in range(len(stats) - 1): 221 | thisstmt, thislevel = stats[i] 222 | nextstmt = stats[i + 1][0] 223 | have = getlspace(lines[thisstmt]) 224 | want = thislevel * 4 225 | if want < 0: 226 | # A comment line. 227 | if have: 228 | # An indented comment line. If we saw the same 229 | # indentation before, reuse what it most recently 230 | # mapped to. 231 | want = have2want.get(have, -1) 232 | if want < 0: 233 | # Then it probably belongs to the next real stmt. 234 | for j in range(i + 1, len(stats) - 1): 235 | jline, jlevel = stats[j] 236 | if jlevel >= 0: 237 | if have == getlspace(lines[jline]): 238 | want = jlevel * 4 239 | break 240 | if want < 0: # Maybe it's a hanging 241 | # comment like this one, 242 | # in which case we should shift it like its base 243 | # line got shifted. 244 | for j in range(i - 1, -1, -1): 245 | jline, jlevel = stats[j] 246 | if jlevel >= 0: 247 | want = have + (getlspace(after[jline - 1]) - 248 | getlspace(lines[jline])) 249 | break 250 | if want < 0: 251 | # Still no luck -- leave it alone. 252 | want = have 253 | else: 254 | want = 0 255 | assert want >= 0 256 | have2want[have] = want 257 | diff = want - have 258 | if diff == 0 or have == 0: 259 | after.extend(lines[thisstmt:nextstmt]) 260 | else: 261 | for line in lines[thisstmt:nextstmt]: 262 | if diff > 0: 263 | if line == "\n": 264 | after.append(line) 265 | else: 266 | after.append(" " * diff + line) 267 | else: 268 | remove = min(getlspace(line), -diff) 269 | after.append(line[remove:]) 270 | return self.raw != self.after 271 | 272 | def write(self, f): 273 | f.writelines(self.after) 274 | 275 | # Line-getter for tokenize. 276 | def getline(self): 277 | if self.index >= len(self.lines): 278 | line = "" 279 | else: 280 | line = self.lines[self.index] 281 | self.index += 1 282 | return line 283 | 284 | # Line-eater for tokenize. 285 | def tokeneater(self, type, token, slinecol, end, line, 286 | INDENT=tokenize.INDENT, 287 | DEDENT=tokenize.DEDENT, 288 | NEWLINE=tokenize.NEWLINE, 289 | COMMENT=tokenize.COMMENT, 290 | NL=tokenize.NL): 291 | 292 | if type == NEWLINE: 293 | # A program statement, or ENDMARKER, will eventually follow, 294 | # after some (possibly empty) run of tokens of the form 295 | # (NL | COMMENT)* (INDENT | DEDENT+)? 296 | self.find_stmt = 1 297 | 298 | elif type == INDENT: 299 | self.find_stmt = 1 300 | self.level += 1 301 | 302 | elif type == DEDENT: 303 | self.find_stmt = 1 304 | self.level -= 1 305 | 306 | elif type == COMMENT: 307 | if self.find_stmt: 308 | self.stats.append((slinecol[0], -1)) 309 | # but we're still looking for a new stmt, so leave 310 | # find_stmt alone 311 | 312 | elif type == NL: 313 | pass 314 | 315 | elif self.find_stmt: 316 | # This is the first "real token" following a NEWLINE, so it 317 | # must be the first token of the next program statement, or an 318 | # ENDMARKER. 319 | self.find_stmt = 0 320 | if line: # not endmarker 321 | self.stats.append((slinecol[0], self.level)) 322 | 323 | 324 | # Count number of leading blanks. 325 | def getlspace(line): 326 | i, n = 0, len(line) 327 | while i < n and line[i] == " ": 328 | i += 1 329 | return i 330 | 331 | 332 | if __name__ == '__main__': 333 | main() 334 | -------------------------------------------------------------------------------- /upgrade_to_python3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | import sys 6 | from subprocess import CalledProcessError, run 7 | from typing import Iterable, Tuple, Union 8 | 9 | from generate_commit_msg import generate_commit_msg 10 | 11 | print('=' * 83) # Mark the start of Python execution in the Action logfile 12 | 13 | NEW_BRANCH_NAME = 'modernize-Python-2-codes' 14 | 15 | # https://github.com/PythonCharmers/python-future/blob/master/src/libfuturize/fixes/__init__.py 16 | # An even safer subset of fixes than `futurize --stage1` 17 | SAFE_FIXES = set('lib2to3.fixes.fix_' + fix for fix 18 | in """apply except exec exitfunc funcattrs has_key idioms intern isinstance 19 | methodattrs ne numliterals paren reduce renames repr standarderror 20 | sys_exc throw tuple_params types xreadlines""".split()) 21 | 22 | 23 | def cmd(in_cmd: Union[str, Iterable[str]], check: bool = True, err_text: bool = False) -> str: # run command and return its output 24 | """Run a command and return its output or raise CalledProcessError""" 25 | print('$', in_cmd) 26 | if isinstance(in_cmd, str): 27 | in_cmd = in_cmd.split() 28 | result = run(in_cmd, capture_output=True, text=True) 29 | if result.stdout: 30 | print(result.stdout.rstrip()) 31 | if result.stderr: 32 | print(result.stderr.rstrip()) 33 | if check: 34 | result.check_returncode() # will raise subprocess.CalledProcessError() 35 | # return '\n'.join(result.stdout.splitlines()) 36 | return result.stdout + (result.stderr if err_text else '') 37 | 38 | 39 | def flake8_tests() -> str: 40 | return cmd('flake8 . --show-source --statistics --select=E999', check=False) 41 | 42 | 43 | def files_with_print_issues(flake8_results: str) -> Tuple[str]: 44 | """Walk backwards through flake8 output to find those files that have old style print statements.""" 45 | file_paths = set() 46 | next_line_contains_file = False 47 | # reverse the lines so we can move from last to first 48 | for line in reversed(flake8_results.splitlines()): 49 | if next_line_contains_file: 50 | file_paths.add(line.split(':')[0]) 51 | next_line_contains_file = 'print ' in line 52 | return tuple(sorted(file_paths)) 53 | 54 | 55 | def fix_print(file_paths: Iterable[str]) -> str: 56 | if not file_paths: 57 | return '' 58 | return cmd('futurize -f libfuturize.fixes.fix_print_with_import -w ' + 59 | ' '.join(file_paths)) 60 | 61 | 62 | def fix_safe_fixes() -> str: 63 | """This is an even safer subset of futurize --stage1 -w .""" 64 | return cmd(f"futurize -f {' -f '.join(SAFE_FIXES)} " 65 | "-f libfuturize.fixes.fix_next_call -w .") 66 | 67 | 68 | # def checkout_new_branch(branch_name: str = '') -> str: 69 | # branch_name = branch_name or NEW_BRANCH_NAME 70 | # return cmd(f'git checkout -b {branch_name}') 71 | 72 | 73 | # def git_remote_add_upstream(upstream_url: str) -> str: 74 | # return cmd(f'git remote add upstream {upstream_url}') 75 | 76 | 77 | # print('os.environ: ' + '\n '.join(f'{key}: {os.getenv(key)}' 78 | #  for key in sorted(os.environ))) 79 | 80 | with open(os.getenv('GITHUB_EVENT_PATH')) as in_file: 81 | github_event = json.load(in_file) 82 | # print(json.dumps(github_event, sort_keys=True, indent=2)) 83 | 84 | flake8_results = flake8_tests() 85 | assert flake8_results, """No Python 3 syntax errors or undefined names were found. 86 | This Action can not propose any further changes.""" 87 | file_paths = files_with_print_issues(flake8_results) 88 | diff = fix_print(file_paths) if file_paths else fix_safe_fixes() 89 | push_result = '' 90 | if '+' not in diff: 91 | print('diff is empty!') 92 | print('Success!') 93 | print('\n'.join(line.replace('remote:', '') for line in push_result.splitlines()[1:4])) 94 | -------------------------------------------------------------------------------- /upgrade_to_python3_hack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import os 5 | 6 | from datetime import datetime as dt 7 | from subprocess import run 8 | from typing import Iterable, Tuple, Union 9 | 10 | assert os.getenv('GITHUB_TOKEN'), 'Need access to the secret GITHUB_TOKEN.' 11 | with open(os.getenv("GITHUB_EVENT_PATH")) as in_file: 12 | github_event = json.load(in_file) 13 | # print(json.dumps(github_event, sort_keys=True, indent=2)) 14 | 15 | 16 | def cmd(in_cmd: Union[str, Iterable[str]], check: bool = True) -> str: # run command and return its output 17 | """Run a command and return its output or raise CalledProcessError""" 18 | print('$', in_cmd) 19 | if isinstance(in_cmd, str): 20 | in_cmd = in_cmd.strip().split() 21 | result = run(in_cmd, capture_output=True, text=True) 22 | if result.stdout: 23 | print(result.stdout.rstrip()) 24 | if result.stderr: 25 | print(result.stderr.rstrip()) 26 | if check: 27 | result.check_returncode() # will raise subprocess.CalledProcessError() 28 | return '\n'.join(result.stdout.splitlines()) 29 | 30 | 31 | def main() -> None: 32 | cmd('git config --global user.email "{head_commit[author][email]}"'.format(**github_event)) 33 | cmd('git config --global user.name "{head_commit[author][name]}"'.format(**github_event)) 34 | cmd('git remote add upstream https://github.com/{}.git'.format(os.getenv('GITHUB_REPOSITORY'))) 35 | cmd('git remote -v') 36 | 37 | idea_name = 'new_idea_{:%Y_%m_%d_%H_%M_%S}'.format(dt.now()) 38 | file_name = idea_name + '.md' # new_idea_2019_02_11_06_39_02.md 39 | 40 | cmd('git checkout -b ' + idea_name) 41 | 42 | with open(file_name, 'w') as out_file: 43 | out_file.write('# My new idea is ' + idea_name) 44 | 45 | cmd('git add ' + file_name) 46 | cmd('git rm .github/main.workflow') 47 | cmd(['git', 'commit', f'-am"Add {idea_name}"']) 48 | cmd('git push --set-upstream origin ' + idea_name) 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | --------------------------------------------------------------------------------