├── TODO ├── LICENSE ├── README.md ├── pyqver3.py └── pyqver2.py /TODO: -------------------------------------------------------------------------------- 1 | - identify syntax changes, eg. "except Exception as x:" or "f(a, *b, k=c)" (2.6) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is provided 'as-is', without any express or implied 2 | warranty. In no event will the author be held liable for any damages 3 | arising from the use of this software. 4 | 5 | Permission is granted to anyone to use this software for any purpose, 6 | including commercial applications, and to alter it and redistribute it 7 | freely, subject to the following restrictions: 8 | 9 | 1. The origin of this software must not be misrepresented; you must not 10 | claim that you wrote the original software. If you use this software 11 | in a product, an acknowledgment in the product documentation would be 12 | appreciated but is not required. 13 | 2. Altered source versions must be plainly marked as such, and must not be 14 | misrepresented as being the original software. 15 | 3. This notice may not be removed or altered from any source distribution. 16 | 17 | Copyright (c) 2009-2013 Greg Hewgill http://hewgill.com 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyqver - query required Python version 2 | 3 | Greg Hewgill 4 | [http://hewgill.com](http://hewgill.com) 5 | 6 | ## INTRODUCTION 7 | 8 | This script attempts to identify the minimum version of Python that is required 9 | to execute a particular source file. 10 | 11 | When developing Python scripts for distribution, it is desirable to identify 12 | which minimum version of the Python interpreter is required. `pyqver` attempts to 13 | answer this question using a simplistic analysis of the output of the Python 14 | compiler. 15 | 16 | When run without the `-v` argument, sources are listed along with the minimum 17 | version of Python required. When run with the `-v` option, each version is 18 | listed along with the reasons why that version is required. For example, for 19 | the `pyqver2.py` script itself: 20 | 21 | pyqver2.py 22 | 2.3 platform 23 | 24 | This means that `pyqver2.py` uses the `platform` module, which is a 2.3 25 | feature. 26 | 27 | The `pyqver2.py` script is specific to Python 2.x, and `pyqver3.py` is specific 28 | to Python 3.x. 29 | 30 | This script was inspired by the following question on Stack Overflow: 31 | [Tool to determine what lowest version of Python required?][1] 32 | 33 | [1]: http://stackoverflow.com/questions/804538/tool-to-determine-what-lowest-version-of-python-required 34 | 35 | ## REQUIREMENTS 36 | 37 | This script requires at least Python 2.3. 38 | 39 | ## USAGE 40 | 41 | Usage: pyqver[23].py [options] source ... 42 | 43 | Report minimum Python version required to run given source files. 44 | 45 | -m x.y or --min-version x.y (default M.N) 46 | report version triggers at or above version x.y in verbose mode 47 | -l or --lint 48 | print a lint style report showing each offending line 49 | -v or --verbose 50 | print more detailed report of version triggers for each version 51 | 52 | `M.N` is the default minimum version depending on whether `pyqver2.py` or 53 | `pyqver3.py` is run. 54 | 55 | ## BUGS 56 | 57 | There are currently a few features which are not detected. For example, the 2.6 58 | syntax 59 | 60 | try: 61 | # ... 62 | except Exception as x: 63 | # ... 64 | 65 | is not detected because the output of the `compiler` module is the same for 66 | both the old and the new syntax. 67 | 68 | The `TODO` file has a few notes of things to do. 69 | -------------------------------------------------------------------------------- /pyqver3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import ast 4 | import platform 5 | import sys 6 | 7 | StandardModules = { 8 | "argparse": (3, 2), 9 | "faulthandler": (3, 3), 10 | "importlib": (3, 1), 11 | "ipaddress": (3, 3), 12 | "lzma": (3, 3), 13 | "tkinter.ttk": (3, 1), 14 | "unittest.mock": (3, 3), 15 | "venv": (3, 3), 16 | } 17 | 18 | Functions = { 19 | "bytearray.maketrans": (3, 1), 20 | "bytes.maketrans": (3, 1), 21 | "bz2.open": (3, 3), 22 | "collections.Counter": (3, 1), 23 | "collections.OrderedDict": (3, 1), 24 | "crypt.mksalt": (3, 3), 25 | "email.generator.BytesGenerator": (3, 2), 26 | "email.message_from_binary_file": (3, 2), 27 | "email.message_from_bytes": (3, 2), 28 | "functools.lru_cache": (3, 2), 29 | "gzip.compress": (3, 2), 30 | "gzip.decompress": (3, 2), 31 | "inspect.getclosurevars": (3, 3), 32 | "inspect.getgeneratorlocals": (3, 3), 33 | "inspect.getgeneratorstate": (3, 2), 34 | "itertools.combinations_with_replacement": (3, 1), 35 | "itertools.compress": (3, 1), 36 | "logging.config.dictConfig": (3, 2), 37 | "logging.NullHandler": (3, 1), 38 | "math.erf": (3, 2), 39 | "math.erfc": (3, 2), 40 | "math.expm1": (3, 2), 41 | "math.gamma": (3, 2), 42 | "math.isfinite": (3, 2), 43 | "math.lgamma": (3, 2), 44 | "math.log2": (3, 3), 45 | "os.environb": (3, 2), 46 | "os.fsdecode": (3, 2), 47 | "os.fsencode": (3, 2), 48 | "os.fwalk": (3, 3), 49 | "os.getenvb": (3, 2), 50 | "os.get_exec_path": (3, 2), 51 | "os.getgrouplist": (3, 3), 52 | "os.getpriority": (3, 3), 53 | "os.getresgid": (3, 2), 54 | "os.getresuid": (3, 2), 55 | "os.get_terminal_size": (3, 3), 56 | "os.getxattr": (3, 3), 57 | "os.initgroups": (3, 2), 58 | "os.listxattr": (3, 3), 59 | "os.lockf": (3, 3), 60 | "os.pipe2": (3, 3), 61 | "os.posix_fadvise": (3, 3), 62 | "os.posix_fallocate": (3, 3), 63 | "os.pread": (3, 3), 64 | "os.pwrite": (3, 3), 65 | "os.readv": (3, 3), 66 | "os.removexattr": (3, 3), 67 | "os.replace": (3, 3), 68 | "os.sched_get_priority_max": (3, 3), 69 | "os.sched_get_priority_min": (3, 3), 70 | "os.sched_getaffinity": (3, 3), 71 | "os.sched_getparam": (3, 3), 72 | "os.sched_getscheduler": (3, 3), 73 | "os.sched_rr_get_interval": (3, 3), 74 | "os.sched_setaffinity": (3, 3), 75 | "os.sched_setparam": (3, 3), 76 | "os.sched_setscheduler": (3, 3), 77 | "os.sched_yield": (3, 3), 78 | "os.sendfile": (3, 3), 79 | "os.setpriority": (3, 3), 80 | "os.setresgid": (3, 2), 81 | "os.setresuid": (3, 2), 82 | "os.setxattr": (3, 3), 83 | "os.sync": (3, 3), 84 | "os.truncate": (3, 3), 85 | "os.waitid": (3, 3), 86 | "os.writev": (3, 3), 87 | "shutil.chown": (3, 3), 88 | "shutil.disk_usage": (3, 3), 89 | "shutil.get_archive_formats": (3, 3), 90 | "shutil.get_terminal_size": (3, 3), 91 | "shutil.get_unpack_formats": (3, 3), 92 | "shutil.make_archive": (3, 3), 93 | "shutil.register_archive_format": (3, 3), 94 | "shutil.register_unpack_format": (3, 3), 95 | "shutil.unpack_archive": (3, 3), 96 | "shutil.unregister_archive_format": (3, 3), 97 | "shutil.unregister_unpack_format": (3, 3), 98 | "shutil.which": (3, 3), 99 | "signal.pthread_kill": (3, 3), 100 | "signal.pthread_sigmask": (3, 3), 101 | "signal.sigpending": (3, 3), 102 | "signal.sigtimedwait": (3, 3), 103 | "signal.sigwait": (3, 3), 104 | "signal.sigwaitinfo": (3, 3), 105 | "socket.CMSG_LEN": (3, 3), 106 | "socket.CMSG_SPACE": (3, 3), 107 | "socket.fromshare": (3, 3), 108 | "socket.if_indextoname": (3, 3), 109 | "socket.if_nameindex": (3, 3), 110 | "socket.if_nametoindex": (3, 3), 111 | "socket.sethostname": (3, 3), 112 | "ssl.match_hostname": (3, 2), 113 | "ssl.RAND_bytes": (3, 3), 114 | "ssl.RAND_pseudo_bytes": (3, 3), 115 | "ssl.SSLContext": (3, 2), 116 | "ssl.SSLEOFError": (3, 3), 117 | "ssl.SSLSyscallError": (3, 3), 118 | "ssl.SSLWantReadError": (3, 3), 119 | "ssl.SSLWantWriteError": (3, 3), 120 | "ssl.SSLZeroReturnError": (3, 3), 121 | "stat.filemode": (3, 3), 122 | "textwrap.indent": (3, 3), 123 | "threading.get_ident": (3, 3), 124 | "time.clock_getres": (3, 3), 125 | "time.clock_gettime": (3, 3), 126 | "time.clock_settime": (3, 3), 127 | "time.get_clock_info": (3, 3), 128 | "time.monotonic": (3, 3), 129 | "time.perf_counter": (3, 3), 130 | "time.process_time": (3, 3), 131 | "types.new_class": (3, 3), 132 | "types.prepare_class": (3, 3), 133 | } 134 | 135 | def uniq(a): 136 | if len(a) == 0: 137 | return [] 138 | else: 139 | return [a[0]] + uniq([x for x in a if x != a[0]]) 140 | 141 | class NodeChecker(ast.NodeVisitor): 142 | def __init__(self): 143 | self.vers = dict() 144 | self.vers[(3,0)] = [] 145 | def add(self, node, ver, msg): 146 | if ver not in self.vers: 147 | self.vers[ver] = [] 148 | self.vers[ver].append((node.lineno, msg)) 149 | def visit_Call(self, node): 150 | def rollup(n): 151 | if isinstance(n, ast.Name): 152 | return n.id 153 | elif isinstance(n, ast.Attribute): 154 | r = rollup(n.value) 155 | if r: 156 | return r + "." + n.attr 157 | name = rollup(node.func) 158 | if name: 159 | v = Functions.get(name) 160 | if v is not None: 161 | self.add(node, v, name) 162 | self.generic_visit(node) 163 | def visit_Import(self, node): 164 | for n in node.names: 165 | v = StandardModules.get(n.name) 166 | if v is not None: 167 | self.add(node, v, n.name) 168 | self.generic_visit(node) 169 | def visit_ImportFrom(self, node): 170 | v = StandardModules.get(node.module) 171 | if v is not None: 172 | self.add(node, v, node.module) 173 | for n in node.names: 174 | name = node.module + "." + n.name 175 | v = Functions.get(name) 176 | if v is not None: 177 | self.add(node, v, name) 178 | def visit_Raise(self, node): 179 | if isinstance(node.cause, ast.Name) and node.cause.id == "None": 180 | self.add(node, (3,3), "raise ... from None") 181 | def visit_YieldFrom(self, node): 182 | self.add(node, (3,3), "yield from") 183 | 184 | def get_versions(source, filename=None): 185 | """Return information about the Python versions required for specific features. 186 | 187 | The return value is a dictionary with keys as a version number as a tuple 188 | (for example Python 3.1 is (3,1)) and the value are a list of features that 189 | require the indicated Python version. 190 | """ 191 | tree = ast.parse(source, filename=filename) 192 | checker = NodeChecker() 193 | checker.visit(tree) 194 | return checker.vers 195 | 196 | def v33(source): 197 | if sys.version_info >= (3, 3): 198 | return qver(source) 199 | else: 200 | print("Not all features tested, run --test with Python 3.3", file=sys.stderr) 201 | return (3, 3) 202 | 203 | def qver(source): 204 | """Return the minimum Python version required to run a particular bit of code. 205 | 206 | >>> qver('print("hello world")') 207 | (3, 0) 208 | >>> qver("import importlib") 209 | (3, 1) 210 | >>> qver("from importlib import x") 211 | (3, 1) 212 | >>> qver("import tkinter.ttk") 213 | (3, 1) 214 | >>> qver("from collections import Counter") 215 | (3, 1) 216 | >>> qver("collections.OrderedDict()") 217 | (3, 1) 218 | >>> qver("import functools\\n@functools.lru_cache()\\ndef f(x): x*x") 219 | (3, 2) 220 | >>> v33("yield from x") 221 | (3, 3) 222 | >>> v33("raise x from None") 223 | (3, 3) 224 | """ 225 | return max(get_versions(source).keys()) 226 | 227 | Verbose = False 228 | MinVersion = (3, 0) 229 | Lint = False 230 | 231 | files = [] 232 | i = 1 233 | while i < len(sys.argv): 234 | a = sys.argv[i] 235 | if a == "--test": 236 | import doctest 237 | doctest.testmod() 238 | sys.exit(0) 239 | if a == "-v" or a == "--verbose": 240 | Verbose = True 241 | elif a == "-l" or a == "--lint": 242 | Lint = True 243 | elif a == "-m" or a == "--min-version": 244 | i += 1 245 | MinVersion = tuple(map(int, sys.argv[i].split("."))) 246 | else: 247 | files.append(a) 248 | i += 1 249 | 250 | if not files: 251 | print("""Usage: {0} [options] source ... 252 | 253 | Report minimum Python version required to run given source files. 254 | 255 | -m x.y or --min-version x.y (default 3.0) 256 | report version triggers at or above version x.y in verbose mode 257 | -v or --verbose 258 | print more detailed report of version triggers for each version 259 | """.format(sys.argv[0]), file=sys.stderr) 260 | sys.exit(1) 261 | 262 | for fn in files: 263 | try: 264 | f = open(fn) 265 | source = f.read() 266 | f.close() 267 | ver = get_versions(source, fn) 268 | if Verbose: 269 | print(fn) 270 | for v in sorted([k for k in ver.keys() if k >= MinVersion], reverse=True): 271 | reasons = [x for x in uniq(ver[v]) if x] 272 | if reasons: 273 | # each reason is (lineno, message) 274 | print("\t{0}\t{1}".format(".".join(map(str, v)), ", ".join(x[1] for x in reasons))) 275 | elif Lint: 276 | for v in sorted([k for k in ver.keys() if k >= MinVersion], reverse=True): 277 | reasons = [x for x in uniq(ver[v]) if x] 278 | for r in reasons: 279 | # each reason is (lineno, message) 280 | print("{0}:{1}: {2} {3}".format(fn, r[0], ".".join(map(str, v)), r[1])) 281 | else: 282 | print("{0}\t{1}".format(".".join(map(str, max(ver.keys()))), fn)) 283 | except SyntaxError as x: 284 | print("{0}: syntax error compiling with Python {1}: {2}".format(fn, platform.python_version(), x)) 285 | -------------------------------------------------------------------------------- /pyqver2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import compiler 4 | import platform 5 | import sys 6 | 7 | StandardModules = { 8 | "__future__": (2, 1), 9 | "abc": (2, 6), 10 | "argparse": (2, 7), 11 | "ast": (2, 6), 12 | "atexit": (2, 0), 13 | "bz2": (2, 3), 14 | "cgitb": (2, 2), 15 | "collections": (2, 4), 16 | "contextlib": (2, 5), 17 | "cookielib": (2, 4), 18 | "cProfile": (2, 5), 19 | "csv": (2, 3), 20 | "ctypes": (2, 5), 21 | "datetime": (2, 3), 22 | "decimal": (2, 4), 23 | "difflib": (2, 1), 24 | "DocXMLRPCServer": (2, 3), 25 | "dummy_thread": (2, 3), 26 | "dummy_threading": (2, 3), 27 | "email": (2, 2), 28 | "fractions": (2, 6), 29 | "functools": (2, 5), 30 | "future_builtins": (2, 6), 31 | "hashlib": (2, 5), 32 | "heapq": (2, 3), 33 | "hmac": (2, 2), 34 | "hotshot": (2, 2), 35 | "HTMLParser": (2, 2), 36 | "importlib": (2, 7), 37 | "inspect": (2, 1), 38 | "io": (2, 6), 39 | "itertools": (2, 3), 40 | "json": (2, 6), 41 | "logging": (2, 3), 42 | "modulefinder": (2, 3), 43 | "msilib": (2, 5), 44 | "multiprocessing": (2, 6), 45 | "netrc": (1, 5, 2), 46 | "numbers": (2, 6), 47 | "optparse": (2, 3), 48 | "ossaudiodev": (2, 3), 49 | "pickletools": (2, 3), 50 | "pkgutil": (2, 3), 51 | "platform": (2, 3), 52 | "pydoc": (2, 1), 53 | "runpy": (2, 5), 54 | "sets": (2, 3), 55 | "shlex": (1, 5, 2), 56 | "SimpleXMLRPCServer": (2, 2), 57 | "spwd": (2, 5), 58 | "sqlite3": (2, 5), 59 | "ssl": (2, 6), 60 | "stringprep": (2, 3), 61 | "subprocess": (2, 4), 62 | "sysconfig": (2, 7), 63 | "tarfile": (2, 3), 64 | "textwrap": (2, 3), 65 | "timeit": (2, 3), 66 | "unittest": (2, 1), 67 | "uuid": (2, 5), 68 | "warnings": (2, 1), 69 | "weakref": (2, 1), 70 | "winsound": (1, 5, 2), 71 | "wsgiref": (2, 5), 72 | "xml.dom": (2, 0), 73 | "xml.dom.minidom": (2, 0), 74 | "xml.dom.pulldom": (2, 0), 75 | "xml.etree.ElementTree": (2, 5), 76 | "xml.parsers.expat":(2, 0), 77 | "xml.sax": (2, 0), 78 | "xml.sax.handler": (2, 0), 79 | "xml.sax.saxutils": (2, 0), 80 | "xml.sax.xmlreader":(2, 0), 81 | "xmlrpclib": (2, 2), 82 | "zipfile": (1, 6), 83 | "zipimport": (2, 3), 84 | "_ast": (2, 5), 85 | "_winreg": (2, 0), 86 | } 87 | 88 | Functions = { 89 | "all": (2, 5), 90 | "any": (2, 5), 91 | "collections.Counter": (2, 7), 92 | "collections.defaultdict": (2, 5), 93 | "collections.OrderedDict": (2, 7), 94 | "enumerate": (2, 3), 95 | "frozenset": (2, 4), 96 | "itertools.compress": (2, 7), 97 | "math.erf": (2, 7), 98 | "math.erfc": (2, 7), 99 | "math.expm1": (2, 7), 100 | "math.gamma": (2, 7), 101 | "math.lgamma": (2, 7), 102 | "memoryview": (2, 7), 103 | "next": (2, 6), 104 | "os.getresgid": (2, 7), 105 | "os.getresuid": (2, 7), 106 | "os.initgroups": (2, 7), 107 | "os.setresgid": (2, 7), 108 | "os.setresuid": (2, 7), 109 | "reversed": (2, 4), 110 | "set": (2, 4), 111 | "subprocess.check_call": (2, 5), 112 | "subprocess.check_output": (2, 7), 113 | "sum": (2, 3), 114 | "symtable.is_declared_global": (2, 7), 115 | "weakref.WeakSet": (2, 7), 116 | } 117 | 118 | Identifiers = { 119 | "False": (2, 2), 120 | "True": (2, 2), 121 | } 122 | 123 | def uniq(a): 124 | if len(a) == 0: 125 | return [] 126 | else: 127 | return [a[0]] + uniq([x for x in a if x != a[0]]) 128 | 129 | class NodeChecker(object): 130 | def __init__(self): 131 | self.vers = dict() 132 | self.vers[(2,0)] = [] 133 | def add(self, node, ver, msg): 134 | if ver not in self.vers: 135 | self.vers[ver] = [] 136 | self.vers[ver].append((node.lineno, msg)) 137 | def default(self, node): 138 | for child in node.getChildNodes(): 139 | self.visit(child) 140 | def visitCallFunc(self, node): 141 | def rollup(n): 142 | if isinstance(n, compiler.ast.Name): 143 | return n.name 144 | elif isinstance(n, compiler.ast.Getattr): 145 | r = rollup(n.expr) 146 | if r: 147 | return r + "." + n.attrname 148 | name = rollup(node.node) 149 | if name: 150 | v = Functions.get(name) 151 | if v is not None: 152 | self.add(node, v, name) 153 | self.default(node) 154 | def visitClass(self, node): 155 | if node.bases: 156 | self.add(node, (2,2), "new-style class") 157 | if node.decorators: 158 | self.add(node, (2,6), "class decorator") 159 | self.default(node) 160 | def visitDictComp(self, node): 161 | self.add(node, (2,7), "dictionary comprehension") 162 | self.default(node) 163 | def visitFloorDiv(self, node): 164 | self.add(node, (2,2), "// operator") 165 | self.default(node) 166 | def visitFrom(self, node): 167 | v = StandardModules.get(node.modname) 168 | if v is not None: 169 | self.add(node, v, node.modname) 170 | for n in node.names: 171 | name = node.modname + "." + n[0] 172 | v = Functions.get(name) 173 | if v is not None: 174 | self.add(node, v, name) 175 | def visitFunction(self, node): 176 | if node.decorators: 177 | self.add(node, (2,4), "function decorator") 178 | self.default(node) 179 | def visitGenExpr(self, node): 180 | self.add(node, (2,4), "generator expression") 181 | self.default(node) 182 | def visitGetattr(self, node): 183 | if (isinstance(node.expr, compiler.ast.Const) 184 | and isinstance(node.expr.value, str) 185 | and node.attrname == "format"): 186 | self.add(node, (2,6), "string literal .format()") 187 | self.default(node) 188 | def visitIfExp(self, node): 189 | self.add(node, (2,5), "inline if expression") 190 | self.default(node) 191 | def visitImport(self, node): 192 | for n in node.names: 193 | v = StandardModules.get(n[0]) 194 | if v is not None: 195 | self.add(node, v, n[0]) 196 | self.default(node) 197 | def visitName(self, node): 198 | v = Identifiers.get(node.name) 199 | if v is not None: 200 | self.add(node, v, node.name) 201 | self.default(node) 202 | def visitSet(self, node): 203 | self.add(node, (2,7), "set literal") 204 | self.default(node) 205 | def visitSetComp(self, node): 206 | self.add(node, (2,7), "set comprehension") 207 | self.default(node) 208 | def visitTryFinally(self, node): 209 | # try/finally with a suite generates a Stmt node as the body, 210 | # but try/except/finally generates a TryExcept as the body 211 | if isinstance(node.body, compiler.ast.TryExcept): 212 | self.add(node, (2,5), "try/except/finally") 213 | self.default(node) 214 | def visitWith(self, node): 215 | if isinstance(node.body, compiler.ast.With): 216 | self.add(node, (2,7), "with statement with multiple contexts") 217 | else: 218 | self.add(node, (2,5), "with statement") 219 | self.default(node) 220 | def visitYield(self, node): 221 | self.add(node, (2,2), "yield expression") 222 | self.default(node) 223 | 224 | def get_versions(source): 225 | """Return information about the Python versions required for specific features. 226 | 227 | The return value is a dictionary with keys as a version number as a tuple 228 | (for example Python 2.6 is (2,6)) and the value are a list of features that 229 | require the indicated Python version. 230 | """ 231 | tree = compiler.parse(source) 232 | checker = compiler.walk(tree, NodeChecker()) 233 | return checker.vers 234 | 235 | def v27(source): 236 | if sys.version_info >= (2, 7): 237 | return qver(source) 238 | else: 239 | print >>sys.stderr, "Not all features tested, run --test with Python 2.7" 240 | return (2, 7) 241 | 242 | def qver(source): 243 | """Return the minimum Python version required to run a particular bit of code. 244 | 245 | >>> qver('print "hello world"') 246 | (2, 0) 247 | >>> qver('class test(object): pass') 248 | (2, 2) 249 | >>> qver('yield 1') 250 | (2, 2) 251 | >>> qver('a // b') 252 | (2, 2) 253 | >>> qver('True') 254 | (2, 2) 255 | >>> qver('enumerate(a)') 256 | (2, 3) 257 | >>> qver('total = sum') 258 | (2, 0) 259 | >>> qver('sum(a)') 260 | (2, 3) 261 | >>> qver('(x*x for x in range(5))') 262 | (2, 4) 263 | >>> qver('class C:\\n @classmethod\\n def m(): pass') 264 | (2, 4) 265 | >>> qver('y if x else z') 266 | (2, 5) 267 | >>> qver('import hashlib') 268 | (2, 5) 269 | >>> qver('from hashlib import md5') 270 | (2, 5) 271 | >>> qver('import xml.etree.ElementTree') 272 | (2, 5) 273 | >>> qver('try:\\n try: pass;\\n except: pass;\\nfinally: pass') 274 | (2, 0) 275 | >>> qver('try: pass;\\nexcept: pass;\\nfinally: pass') 276 | (2, 5) 277 | >>> qver('from __future__ import with_statement\\nwith x: pass') 278 | (2, 5) 279 | >>> qver('collections.defaultdict(list)') 280 | (2, 5) 281 | >>> qver('from collections import defaultdict') 282 | (2, 5) 283 | >>> qver('"{0}".format(0)') 284 | (2, 6) 285 | >>> qver('memoryview(x)') 286 | (2, 7) 287 | >>> v27('{1, 2, 3}') 288 | (2, 7) 289 | >>> v27('{x for x in s}') 290 | (2, 7) 291 | >>> v27('{x: y for x in s}') 292 | (2, 7) 293 | >>> qver('from __future__ import with_statement\\nwith x:\\n with y: pass') 294 | (2, 5) 295 | >>> v27('from __future__ import with_statement\\nwith x, y: pass') 296 | (2, 7) 297 | >>> qver('@decorator\\ndef f(): pass') 298 | (2, 4) 299 | >>> qver('@decorator\\nclass test:\\n pass') 300 | (2, 6) 301 | 302 | #>>> qver('0o0') 303 | #(2, 6) 304 | #>>> qver('@foo\\nclass C: pass') 305 | #(2, 6) 306 | """ 307 | return max(get_versions(source).keys()) 308 | 309 | Verbose = False 310 | MinVersion = (2, 3) 311 | Lint = False 312 | 313 | files = [] 314 | i = 1 315 | while i < len(sys.argv): 316 | a = sys.argv[i] 317 | if a == "--test": 318 | import doctest 319 | doctest.testmod() 320 | sys.exit(0) 321 | if a == "-v" or a == "--verbose": 322 | Verbose = True 323 | elif a == "-l" or a == "--lint": 324 | Lint = True 325 | elif a == "-m" or a == "--min-version": 326 | i += 1 327 | MinVersion = tuple(map(int, sys.argv[i].split("."))) 328 | else: 329 | files.append(a) 330 | i += 1 331 | 332 | if not files: 333 | print >>sys.stderr, """Usage: %s [options] source ... 334 | 335 | Report minimum Python version required to run given source files. 336 | 337 | -m x.y or --min-version x.y (default 2.3) 338 | report version triggers at or above version x.y in verbose mode 339 | -v or --verbose 340 | print more detailed report of version triggers for each version 341 | """ % sys.argv[0] 342 | sys.exit(1) 343 | 344 | for fn in files: 345 | try: 346 | f = open(fn) 347 | source = f.read() 348 | f.close() 349 | ver = get_versions(source) 350 | if Verbose: 351 | print fn 352 | for v in sorted([k for k in ver.keys() if k >= MinVersion], reverse=True): 353 | reasons = [x for x in uniq(ver[v]) if x] 354 | if reasons: 355 | # each reason is (lineno, message) 356 | print "\t%s\t%s" % (".".join(map(str, v)), ", ".join([x[1] for x in reasons])) 357 | elif Lint: 358 | for v in sorted([k for k in ver.keys() if k >= MinVersion], reverse=True): 359 | reasons = [x for x in uniq(ver[v]) if x] 360 | for r in reasons: 361 | # each reason is (lineno, message) 362 | print "%s:%s: %s %s" % (fn, r[0], ".".join(map(str, v)), r[1]) 363 | else: 364 | print "%s\t%s" % (".".join(map(str, max(ver.keys()))), fn) 365 | except SyntaxError, x: 366 | print "%s: syntax error compiling with Python %s: %s" % (fn, platform.python_version(), x) 367 | --------------------------------------------------------------------------------