├── .gitignore ├── Makefile ├── README.md ├── Small.md ├── logo.svg ├── logo_sml.png ├── requirements.txt ├── setup.py └── subpy ├── __init__.py ├── features.py ├── stdlib.py ├── tests ├── __init__.py └── test_features.py └── validate.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.sw[pon] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python -m unittest discover subpy/tests 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Subpy 6 | ===== 7 | 8 | Subpy is a library for defining subsets of the Python language 9 | and querying ASTs for language-level properties that are 10 | specified as sets of features. 11 | 12 | Many projects aim to work with specific subsets of Python that 13 | are amenable to *static analysis* and *type inference*, subpy is 14 | simply a static analysis library for checking for subsets with 15 | the intention of providing more informative error reporting for 16 | end-users. 17 | 18 | Usage 19 | ----- 20 | 21 | The input to the ``checker`` can be either a Module, Function or 22 | source code as string. It returns a dictionary of lists keyed by 23 | the ``feature`` enumeration code and the values with the line 24 | numbers where the feature is detected. 25 | 26 | ```python 27 | >>> from subpy import checker 28 | >>> import io 29 | 30 | >>> print checker(io) 31 | 32 | {3: [98], 33 | 5: [78, 81, 84, 87], 34 | 9: [78, 81, 84, 87], 35 | 10: [81, 84, 87], 36 | 32: [92, 96], 37 | 34: [79]} 38 | ``` 39 | 40 | Matching the feature codes with the keys in the dictionary we see 41 | the information this is telling us that in the ``io`` module in 42 | the standard library: 43 | 44 | * A *delete* is used on line 98. 45 | * A *class* is used on line 78, 81, 84, and 87. 46 | * *Inheritance* is used on line 78, 81, 84, and 87. 47 | * *Multiple inheritance* is used on line 81, 84, and 87. 48 | * A *custom iterator* is used on line 92 and 96. 49 | * A *metaclass* is used on line 79. 50 | 51 | A example using the function level checker: 52 | 53 | ```python 54 | from subpy import checker 55 | from subpy.features import ListComp 56 | 57 | def example1(): 58 | return [x**2 for x in range(25)] 59 | 60 | def example2(): 61 | return 'hello' 62 | 63 | features = checker(example1) 64 | 65 | if ListComp in features: 66 | print 'You used a list comprehension on lines %r' % (features[ListComp]) 67 | 68 | features = checker(example2) 69 | 70 | if ListComp not in features: 71 | print 'You did not use any list comprehensions!' 72 | 73 | ``` 74 | 75 | Defining Subsets 76 | ---------------- 77 | 78 | For example if we want to exclude the use of *List Comprehensions* 79 | and *Set Comprehensions* we could define a subset of Python that 80 | excludes these features. 81 | 82 | ```python 83 | MyPythonSubset = FullPython - { ListComp, SetComp } 84 | ``` 85 | 86 | The ``validator`` command can be used to raise when unsupported 87 | features are detected in the given source. For example, we'll 88 | support the python feature set excluding list comprehensions and 89 | set comprehensions. 90 | 91 | ```python 92 | from subpy import validator, FullPython, FeatureNotSupported 93 | from subpy.features import ListComp, SetComp 94 | 95 | def example(): 96 | return [x**2 for x in range(25)] 97 | 98 | my_features = FullPython - { ListComp, SetComp } 99 | 100 | validator(example, features=my_features) 101 | ``` 102 | 103 | ```python 104 | File "", line 2 105 | return [x**2 for x in range(25)] 106 | ^ 107 | subpy.validate.FeatureNotSupported: ListComp 108 | ``` 109 | 110 | Subpy is currently able to parse the entire standard library and 111 | can be used to query some interesting trivia facts. 112 | 113 | ```python 114 | from subpy import detect 115 | from subpy.stdlib import libraries 116 | from subpy.features import Metaclasses, MInheritance, Exec 117 | 118 | import importlib 119 | 120 | print('Libraries with Multiple Inheritance and Metaclasses:') 121 | for lib in libraries: 122 | mod = importlib.import_module(lib) 123 | features = detect(mod) 124 | 125 | if Metaclasses in features and MInheritance in features: 126 | print(lib) 127 | 128 | ``` 129 | 130 | ``` 131 | Libraries with Multiple Inheritance and Metaclasses: 132 | io 133 | ``` 134 | 135 | Or to query for potentially unsafe code execution: 136 | 137 | ```python 138 | print('Libraries with Exec') 139 | for lib in libraries: 140 | mod = importlib.import_module(lib) 141 | features = detect(mod) 142 | 143 | if Exec in features: 144 | print(lib) 145 | ``` 146 | 147 | ``` 148 | Libraries with Exec 149 | ihooks 150 | site 151 | cgi 152 | rexec 153 | Bastion 154 | imputil 155 | trace 156 | timeit 157 | cProfile 158 | doctest 159 | code 160 | bdb 161 | runpy 162 | profile 163 | collections 164 | ``` 165 | 166 | Feature Codes 167 | ------------- 168 | 169 | Currently supported features are an enumeration with values given 170 | below: 171 | 172 | 1. ImplicitCasts 173 | 1. Generators 174 | 1. DelVar 175 | 1. Closures 176 | 1. Classes 177 | 1. Decorators 178 | 1. VarArgs 179 | 1. KeywordArgs 180 | 1. Inheritance 181 | 1. MInheritance 182 | 1. ClassDecorators 183 | 1. Assertions 184 | 1. ChainComparison 185 | 1. Exceptions 186 | 1. Lambda 187 | 1. RelativeImports 188 | 1. ImportStar 189 | 1. HeteroList 190 | 1. Continue 191 | 1. MultipleReturn 192 | 1. DictComp 193 | 1. Ellipsi 194 | 1. TupleUnpacking 195 | 1. Exec 196 | 1. FancyIndexing 197 | 1. Globals 198 | 1. ContextManagers 199 | 1. GeneratorExp 200 | 1. Ternary 201 | 1. ListComp 202 | 1. SetComp 203 | 1. CustomIterators 204 | 1. Printing 205 | 1. Metaclasses 206 | 207 | Testing 208 | ------- 209 | 210 | To test run: 211 | 212 | ```bash 213 | $ python -m unittest discover subpy/tests 214 | ``` 215 | 216 | Copying 217 | ------- 218 | 219 | The core logic is self-contained in ``features.py`` and 220 | ``validate.py`` which will function as standalone modules without 221 | subpy package, which includes the test suite. There are no 222 | dependencies other than the standard library. 223 | 224 | License 225 | ------- 226 | 227 | Copyright (c) 2013, Continuum Analytics, Inc. 228 | All rights reserved. 229 | 230 | Redistribution and use in source and binary forms, with or without 231 | modification, are permitted provided that the following conditions are 232 | met: 233 | 234 | Redistributions of source code must retain the above copyright notice, 235 | this list of conditions and the following disclaimer. 236 | 237 | Redistributions in binary form must reproduce the above copyright 238 | notice, this list of conditions and the following disclaimer in the 239 | documentation and/or other materials provided with the distribution. 240 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 241 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 242 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 243 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 244 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 245 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 246 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 247 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 248 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 249 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 250 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 251 | -------------------------------------------------------------------------------- /Small.md: -------------------------------------------------------------------------------- 1 | SmallPy Language Specification 2 | ------------------------------ 3 | 4 | Motivation: 5 | 6 | - Establish a "Numeric Recipies in C" subset of Python. Just 7 | unambiguously make numeric loops fast, do this well because it 8 | generates revenue for Continuum. Then keep the "clever path" as 9 | an ongoing project into how to type larger sets of Python. 10 | 11 | - Allow perfect reasoning about performance by demarcating a 12 | subset of Python that either lowers into LLVM or fails to 13 | compile. 14 | 15 | - If code compiles it is guaranteed to be faster than the 16 | equivalent CPython. 17 | 18 | - Improve the overall user experience of error reporting so that 19 | the end-user knows how to change their code to match the 20 | subset. 21 | 22 | - Explicit is better than implicit. If the end user wants to go 23 | through the Object Layer make them say so. 24 | 25 | Types 26 | ----- 27 | 28 | - int 29 | - float 30 | - bool 31 | - string 32 | - unicode 33 | - complex 34 | - list 35 | - tuple 36 | - NoneType 37 | - FunctionType 38 | 39 | Unsupported Types: 40 | 41 | - object 42 | - dict 43 | - set 44 | - long 45 | - frozenset 46 | - buffer 47 | - bytearray 48 | - bytes 49 | - basestring 50 | - memoryview 51 | 52 | Values 53 | ------ 54 | 55 | Tuple unpacking is not supported. 56 | 57 | Assignment to tuples is syntactic sugar for multiple assignment 58 | only when r-values are literals. 59 | 60 | Supported: 61 | 62 | ``` 63 | a, b = 1, True 64 | ``` 65 | 66 | Not Supported: 67 | 68 | ``` 69 | a, b = f() 70 | ``` 71 | 72 | Control Flow 73 | ------------ 74 | 75 | Supported: 76 | 77 | - If 78 | - If/Else 79 | - If/ElseIf/Else 80 | - For 81 | - While 82 | 83 | Not Supported: 84 | 85 | - Generators 86 | - Exceptions 87 | - Try 88 | - Try/Finally 89 | - Try/Except 90 | - Try/Except/Finally 91 | - While/Else 92 | - For/Else 93 | 94 | Data Model 95 | ---------- 96 | 97 | - Lists are stricly homogeneus containers of data. The 98 | storage is compiler specific. 99 | - Tuples are heteroegneous immutable types. The storage is 100 | compiler specific. 101 | - Strings are variable immutable arrays of ascii characters. The 102 | storage is compiler specific. 103 | - Unicode strings are immutable arrays of UTF-8 characters. The 104 | storage is compiler specific. 105 | 106 | The ``list`` constructor polymorphically lifts elements into 107 | singleton containers. 108 | 109 | ``` 110 | list :: a -> [a] 111 | ``` 112 | 113 | Introspection 114 | ------------- 115 | 116 | Runtime introspection with ``type``, ``isinstance``, 117 | ``issubclass``, ``id``, ``globals``, ``locals``, ``dir``, 118 | ``callable``, ``getattr``, ``hash``, ``hasattr``, ``super``, 119 | ``vars`` is not supported. 120 | 121 | The ``intern`` function is not supported. 122 | 123 | Length 124 | ------ 125 | 126 | The implementation of the ``len`` function is polymorphic and 127 | container specific. 128 | 129 | ```python 130 | len :: [a] -> int 131 | ``` 132 | 133 | Destruction 134 | ----------- 135 | 136 | Variable and element destruction is not supported. The ``del`` 137 | operator is not part of the syntax and ``delattr` is not 138 | supported. 139 | 140 | Metaprogramming 141 | --------------- 142 | 143 | ``compile``, ``eval`` and ``exec``, ``execfile`` are not supported 144 | 145 | Pass 146 | ---- 147 | 148 | Pass is a syntactic construct that translates into a compiler 149 | specific noop. 150 | 151 | System IO 152 | --------- 153 | 154 | ``file``, ``open`` and ``quit``, ``raw_input``, ``reload``, 155 | ``help`` and ``input`` are not supported. 156 | 157 | ``print`` is optionally supported. The implementation is specific 158 | to the compiler. For compilers that do not choose to implement 159 | print it should translate to noop. 160 | 161 | The ``repr`` function is not supported. 162 | 163 | Formatting 164 | ---------- 165 | 166 | Printf style formatting is not supported. The ``Mod`` operator 167 | exclusively maps to numeric modulus. 168 | 169 | Iterators 170 | --------- 171 | 172 | Generators are not supported. 173 | 174 | Range iterators are syntactic sugar for looping constructs. Custom 175 | iterators are not supported. The ``iter`` and ``next`` functions 176 | are not supported. 177 | 178 | ``` 179 | for i in xrange(start, stop, step) 180 | foo() 181 | ``` 182 | 183 | Is lowered into some equivalent low-level looping construct that 184 | roughly corresponds to the following C code: 185 | 186 | ``` 187 | for (i = start; i < stop; i += step) { 188 | foo(); 189 | } 190 | ``` 191 | 192 | The value of ``i`` after the loop block follows the Python 193 | semantics and is set to the last value in the iterator instead of 194 | the C semantics. 195 | 196 | ``xrange`` and `range`` are lowered into the same constructs. 197 | 198 | ``enumerate`` is not supported 199 | 200 | Comprehensions 201 | -------------- 202 | 203 | TODO 204 | 205 | Builtins 206 | -------- 207 | 208 | * abs - Supported 209 | * all - Supported 210 | * any - Supported 211 | * apply - Not Supported 212 | * basestring - Not Supported 213 | * bin - Not Supported 214 | * bool - Supported 215 | * buffer - Not Supported 216 | * bytearray - Not Supported 217 | * bytes - Not Supported 218 | * callable - Not Supported 219 | * chr - Not Supported 220 | * classmethod - Not Supported 221 | * cmp - Supported 222 | * coerce - Not Supported 223 | * compile - Not Supported 224 | * complex - Supported 225 | * copyright - Not Supported 226 | * credits - Not Supported 227 | * delattr - Not Supported 228 | * dict - Not Supported 229 | * dir - Not Supported 230 | * divmod - Supported 231 | * enumerate - Not Supported 232 | * eval - Not Supported 233 | * execfile - Not Supported 234 | * exit - Not Supported 235 | * file - Not Supported 236 | * filter - Supported 237 | * float - Supported 238 | * format - Not Supported 239 | * frozenset - Not Supported 240 | * getattr - Not Supported 241 | * globals - Not Supported 242 | * hasattr - Not Supported 243 | * hash - Not Supported 244 | * help - Not Supported 245 | * hex - Not Supported 246 | * id - Not Supported 247 | * input - Not Supported 248 | * int - Supported 249 | * intern - Not Supported 250 | * isinstance - Not Supported 251 | * issubclass - Not Supported 252 | * iter - Not Supported 253 | * len - Supported 254 | * license - Not Supported 255 | * list - Supported 256 | * locals - Not Supported 257 | * long - Not Supported 258 | * map - Supported 259 | * max - Supported 260 | * memoryview - Not Supported 261 | * min - Supported 262 | * next - Not Supported 263 | * object - Not Supported 264 | * oct - Not Supported 265 | * open - Not Supported 266 | * ord - Not Supported 267 | * pow - Supported 268 | * print - Not Supported 269 | * property - Not Supported 270 | * quit - Not Supported 271 | * range - Supported 272 | * raw_input - Not Supported 273 | * reduce - Supported 274 | * reload - Not Supported 275 | * repr - Not Supported 276 | * reversed - Not Supported 277 | * round - Supported 278 | * set - Not Supported 279 | * setattr - Not Supported 280 | * slice - Not Supported 281 | * sorted - Supported 282 | * staticmethod - Not Supported 283 | * str - Supported 284 | * sum - Supported 285 | * super - Not Supported 286 | * tuple - Not Supported 287 | * type - Not Supported 288 | * unichr - Not Supported 289 | * unicode - Supported 290 | * vars - Not Supported 291 | * xrange - Supported 292 | * zip - Supported 293 | 294 | 295 | Filter 296 | ------ 297 | 298 | ```python 299 | filter :: (a -> bool) -> [a] -> [a] 300 | def filter(p, xs): 301 | return [x for x in xs if p(x)] 302 | ``` 303 | 304 | All 305 | --- 306 | 307 | 308 | ```python 309 | all :: (a -> bool) -> [a] -> bool 310 | def all(p, xs): 311 | for i in xs: 312 | if not p(i): 313 | return False 314 | return True 315 | ``` 316 | 317 | Any 318 | --- 319 | 320 | ```python 321 | any :: (a -> bool) -> [a] -> bool 322 | def any(p, xs): 323 | for i in xs: 324 | if not p(i): 325 | return True 326 | return False 327 | ``` 328 | 329 | Map 330 | --- 331 | 332 | ```python 333 | map :: (a -> b) -> [a] -> [b] 334 | def map(f, xs): 335 | return [f(x) for x in xs] 336 | ``` 337 | 338 | Reduce 339 | ------ 340 | 341 | ```python 342 | reduce :: (a -> b -> a) -> a -> [b] -> a 343 | def reduce(p, xs, x0): 344 | acc = x0 345 | for x in xs: 346 | acc = f(acc) 347 | return acc 348 | ``` 349 | 350 | Max 351 | --- 352 | 353 | ```python 354 | 355 | max :: [a] -> a 356 | def max(xs): 357 | assert len(xs) > 0 358 | x0 = xs[0] 359 | for x in xs: 360 | if x > x0: 361 | x0 = x 362 | return x0 363 | ``` 364 | 365 | Min 366 | --- 367 | 368 | ```python 369 | 370 | min :: [a] -> a 371 | def min(xs): 372 | assert len(xs) > 0 373 | x0 = xs[0] 374 | for x in xs: 375 | if x < x0: 376 | x0 = x 377 | return x0 378 | ``` 379 | 380 | Zip 381 | --- 382 | 383 | ```python 384 | min :: [a] -> [b] -> [(a, b)] 385 | def min(xs, ys): 386 | out = [] 387 | for i in min(len(xs), len(ys)): 388 | out.append((xs[i], ys[i])) 389 | return out 390 | ``` 391 | 392 | Reverse 393 | ------- 394 | 395 | ``` 396 | reverse :: [a] -> [a] 397 | def reverse(xs): 398 | out = [] 399 | for i in len(xs): 400 | out.append(xs[len(xs)-i]) 401 | return out 402 | ``` 403 | 404 | Sorted 405 | ------ 406 | 407 | Sorted sorts the underlying list data structure. The 408 | implementation is compiler specific. 409 | 410 | ```python 411 | sorted :: [a] -> [a] 412 | ``` 413 | 414 | Slice 415 | ----- 416 | 417 | Named slicing is not supported. Slice types are not supported. 418 | Slicing as an indexing operation is supported. 419 | 420 | ``` 421 | a = slice(0, 1, 2) 422 | ``` 423 | 424 | Classes 425 | ------- 426 | 427 | Classes are not supported. The corresponding descriptor methods 428 | are not implemented. 429 | 430 | - property 431 | - classmethod 432 | - staticmethod 433 | 434 | Casts 435 | ----- 436 | 437 | ``` 438 | int :: a -> int 439 | bool :: a -> bool 440 | complex :: a -> bool 441 | ``` 442 | 443 | The coerce function is not supported. 444 | 445 | The ``str``, ``list`` and ``tuple`` casts are not supported. 446 | 447 | Characters 448 | ---------- 449 | 450 | The ``chr``, ``ord`` and ``unichr``, ``hex``, ``bin``, ``oct`` 451 | functions are not supported. 452 | 453 | Closures 454 | -------- 455 | 456 | Nested functions and closures are not supported. 457 | 458 | Globals 459 | ------- 460 | 461 | Global variables are not supported. 462 | 463 | Arguments 464 | --------- 465 | 466 | Variadic and keyword arguments are not supported. 467 | 468 | Assertions 469 | ---------- 470 | 471 | Assertions are not supported. 472 | 473 | Syntax 474 | ------ 475 | 476 | Language Features 477 | 478 | ``` 479 | 480 | module SmallPython 481 | { 482 | mod = Module(stmt* body) 483 | | Expression(expr body) 484 | 485 | stmt = FunctionDef(identifier name, arguments args, 486 | stmt* body, expr? returns) 487 | | Return(expr? value) 488 | 489 | | Assign(expr* targets, expr value) 490 | | AugAssign(expr target, operator op, expr value) 491 | 492 | | For(expr target, expr iter, stmt* body, stmt* orelse) 493 | | While(expr test, stmt* body, stmt* orelse) 494 | | If(expr test, stmt* body, stmt* orelse) 495 | 496 | | Expr(expr value) 497 | | Pass | Break | Continue 498 | 499 | attributes (int lineno, int col_offset) 500 | 501 | expr = BoolOp(boolop op, expr* values) 502 | | BinOp(expr left, operator op, expr right) 503 | | UnaryOp(unaryop op, expr operand) 504 | | IfExp(expr test, expr body, expr orelse) 505 | | ListComp(expr elt, comprehension* generators) 506 | | Compare(expr left, cmpop* ops, expr* comparators) 507 | | Call(expr func, expr* args, keyword* keywords, 508 | expr? starargs, expr? kwargs) 509 | | Num(object n) -- a number as a PyObject. 510 | | Str(string s) -- need to specify raw, unicode, etc? 511 | | Bytes(string s) 512 | 513 | | Attribute(expr value, identifier attr, expr_context ctx) 514 | | Subscript(expr value, slice slice, expr_context ctx) 515 | | Name(identifier id, expr_context ctx) 516 | | List(expr* elts, expr_context ctx) 517 | | Tuple(expr* elts, expr_context ctx) 518 | 519 | attributes (int lineno, int col_offset) 520 | 521 | expr_context = Load | Store | AugLoad | AugStore | Param 522 | 523 | slice = Slice(expr? lower, expr? upper, expr? step) 524 | | ExtSlice(slice* dims) 525 | | Index(expr value) 526 | 527 | boolop = And | Or 528 | 529 | operator = Add | Sub | Mult | Div | Mod | Pow | LShift 530 | | RShift | BitOr | BitXor | BitAnd | FloorDiv 531 | 532 | unaryop = Invert | Not | UAdd | USub 533 | 534 | cmpop = Eq | NotEq | Lt | LtE | Gt | GtE 535 | 536 | comprehension = (expr target, expr iter, expr* ifs) 537 | 538 | arguments = (arg* args, identifier? vararg, expr? varargannotation, 539 | arg* kwonlyargs, identifier? kwarg, 540 | expr? kwargannotation, expr* defaults, 541 | expr* kw_defaults) 542 | arg = (identifier arg, expr? annotation) 543 | 544 | keyword = (identifier arg, expr value) 545 | alias = (identifier name, identifier? asname) 546 | } 547 | 548 | ``` 549 | 550 | Operators 551 | --------- 552 | 553 | - And 554 | - Or 555 | - Add 556 | - Sub 557 | - Mult 558 | - Div 559 | - Mod 560 | - Pow 561 | - LShift 562 | - RShift 563 | - BitOr 564 | - BitXor 565 | - BitAnd 566 | - FloorDiv 567 | - Invert 568 | - Not 569 | - UAdd 570 | - USub 571 | - Eq 572 | - NotEq 573 | - Lt 574 | - LtE 575 | - Gt 576 | - GtE 577 | 578 | Comparison operator chaining is supported and is desugared into 579 | boolean conjunctions of the comparison operators. 580 | 581 | ``` 582 | (x > y > z) 583 | ``` 584 | 585 | ``` 586 | (x > y) and (y > z) 587 | ``` 588 | 589 | Smallpy explictly does not support the following operators. 590 | 591 | - Is 592 | - IsNot 593 | - In 594 | - NotIn 595 | 596 | Division 597 | -------- 598 | 599 | Division follows the Python semantics for distinction between 600 | ``floordiv`` and ``truediv`` but operates over unboxed types 601 | with no error checking. 602 | 603 | Math Functions 604 | -------------- 605 | 606 | - abs 607 | - cmp 608 | - divmod 609 | - pow 610 | - round 611 | 612 | sorted 613 | 614 | Floating Point Math 615 | ------------------- 616 | 617 | ``` 618 | acos 619 | acosh 620 | asin 621 | asinh 622 | atan 623 | atan2 624 | atanh 625 | ceil 626 | copysign 627 | cos 628 | cosh 629 | degrees 630 | e 631 | erf 632 | erfc 633 | exp 634 | expm1 635 | fabs 636 | factorial 637 | floor 638 | fmod 639 | frexp 640 | fsum 641 | gamma 642 | hypot 643 | isinf 644 | isnan 645 | ldexp 646 | lgamma 647 | log 648 | log10 649 | log1p 650 | modf 651 | pi 652 | pow 653 | radians 654 | sin 655 | sinh 656 | sqrt 657 | tan 658 | tanh 659 | trunc 660 | ``` 661 | 662 | Complex Math 663 | 664 | ``` 665 | acos 666 | acosh 667 | asin 668 | asinh 669 | atan 670 | atanh 671 | cos 672 | cosh 673 | exp 674 | isinf 675 | isnan 676 | log 677 | log10 678 | phase 679 | polar 680 | rect 681 | sin 682 | sinh 683 | sqrt 684 | tan 685 | tanh 686 | ``` 687 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 31 | 37 | 38 | 45 | 51 | 52 | 59 | 65 | 66 | 77 | 79 | 83 | 87 | 88 | 98 | 100 | 104 | 108 | 109 | 119 | 121 | 125 | 129 | 130 | 140 | 151 | 153 | 157 | 161 | 162 | 172 | 174 | 178 | 182 | 183 | 193 | 195 | 199 | 203 | 204 | 214 | 224 | 234 | 244 | 255 | 265 | 275 | 286 | 287 | 309 | 311 | 312 | 314 | image/svg+xml 315 | 317 | 318 | 319 | 320 | 321 | 326 | 329 | 334 | 339 | 349 | 350 | 353 | 358 | 363 | 373 | 374 | 377 | 388 | 389 | 390 | 391 | -------------------------------------------------------------------------------- /logo_sml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdiehl/subpy/60e2bb9c9e7f542a121999cc324c4569ff2d23b2/logo_sml.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdiehl/subpy/60e2bb9c9e7f542a121999cc324c4569ff2d23b2/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name = "subpy", 5 | license = "BSD", 6 | description = "Python subsets", 7 | packages = ['subpy', 8 | 'subpy.tests',], 9 | version = "0.1", 10 | ) 11 | -------------------------------------------------------------------------------- /subpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .features import * 2 | from .validate import detect, fd, checker, validator, \ 3 | FeatureNotSupported, FullPython 4 | 5 | 6 | from .tests.test_features import run 7 | 8 | test = run 9 | -------------------------------------------------------------------------------- /subpy/features.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------ 2 | # Feature Set 3 | #------------------------------------------------------------------------ 4 | 5 | ImplicitCasts = 1 6 | Generators = 2 7 | DelVar = 3 8 | Closures = 4 9 | Classes = 5 10 | Decorators = 6 11 | VarArgs = 7 12 | KeywordArgs = 8 13 | Inheritance = 9 14 | MInheritance = 10 15 | ClassDecorators = 11 16 | Assertions = 12 17 | ChainComparison = 13 18 | Exceptions = 14 19 | Lambda = 15 20 | RelativeImports = 16 21 | ImportStar = 17 22 | HeteroList = 18 23 | Continue = 19 24 | MultipleReturn = 20 25 | DictComp = 21 26 | Ellipsi = 22 27 | TupleUnpacking = 23 28 | Exec = 24 29 | FancyIndexing = 25 30 | Globals = 26 31 | ContextManagers = 27 32 | GeneratorExp = 28 33 | Ternary = 29 34 | ListComp = 30 35 | SetComp = 31 36 | CustomIterators = 32 37 | Printing = 33 38 | Metaclasses = 34 39 | -------------------------------------------------------------------------------- /subpy/stdlib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import distutils.sysconfig as sysconfig 3 | 4 | jokes = ['antigravity', 'this', 'dbhash', 'bsddb'] 5 | 6 | def standard_library(): 7 | std_lib = sysconfig.get_python_lib(standard_lib=True) 8 | 9 | for top, dirs, files in os.walk(std_lib): 10 | for nm in files: 11 | prefix = top[len(std_lib)+1:] 12 | if prefix[:13] == 'site-packages': 13 | continue 14 | 15 | if nm == '__init__.py': 16 | pack = top[len(std_lib)+1:].replace(os.path.sep,'.') 17 | 18 | if ('.' not in pack) and\ 19 | (not pack.startswith('_')) and\ 20 | (pack not in jokes): 21 | yield pack 22 | 23 | elif nm[-3:] == '.py': 24 | pack = os.path.join(prefix, nm)[:-3].replace(os.path.sep,'.') 25 | 26 | if ('.' not in pack) and\ 27 | (not pack.startswith('_')) and\ 28 | (pack not in jokes): 29 | yield pack 30 | 31 | # Python 3.x, todo, this has changed 32 | libraries = [ 33 | 'ihooks', 34 | 'quopri', 35 | 'DocXMLRPCServer', 36 | 'shelve', 37 | 'tarfile', 38 | 'CGIHTTPServer', 39 | 'linecache', 40 | 'pdb', 41 | 'sre_parse', 42 | 'user', 43 | 'site', 44 | 'pty', 45 | 'commands', 46 | 'opcode', 47 | 'uu', 48 | 'multifile', 49 | 'pkgutil', 50 | 'dumbdbm', 51 | 'cgi', 52 | 'telnetlib', 53 | 'copy', 54 | 'copy_reg', 55 | 'SimpleXMLRPCServer', 56 | 'textwrap', 57 | 'xmlrpclib', 58 | 'MimeWriter', 59 | 'threading', 60 | 'mailbox', 61 | 'gettext', 62 | 'contextlib', 63 | 'sndhdr', 64 | 'HTMLParser', 65 | 'hashlib', 66 | 'BaseHTTPServer', 67 | 'posixfile', 68 | 'csv', 69 | 'struct', 70 | 'urlparse', 71 | 'rexec', 72 | 'htmllib', 73 | 'asynchat', 74 | 'sha', 75 | 'dis', 76 | 'warnings', 77 | 'heapq', 78 | 'types', 79 | 'token', 80 | 'netrc', 81 | 'mimetypes', 82 | 'anydbm', 83 | 'Bastion', 84 | 'subprocess', 85 | 'imputil', 86 | 'imghdr', 87 | 'macurl2path', 88 | 'trace', 89 | 'atexit', 90 | 'rfc822', 91 | 'imaplib', 92 | 'getpass', 93 | 'md5', 94 | 'base64', 95 | 'sre', 96 | 'cookielib', 97 | 'fileinput', 98 | 'keyword', 99 | 'tty', 100 | 'mutex', 101 | 'timeit', 102 | 'markupbase', 103 | 'stat', 104 | 'mimetools', 105 | 'fpformat', 106 | 'pprint', 107 | 'httplib', 108 | 'fnmatch', 109 | 'zipfile', 110 | 'mhlib', 111 | 'ssl', 112 | 'cProfile', 113 | 'aifc', 114 | 'ftplib', 115 | 'pstats', 116 | 'optparse', 117 | 'dummy_threading', 118 | 'sre_compile', 119 | 'py_compile', 120 | 'uuid', 121 | 'stringold', 122 | 'sunau', 123 | 'Cookie', 124 | 'asyncore', 125 | 'wave', 126 | 'sgmllib', 127 | 'new', 128 | 'getopt', 129 | 'audiodev', 130 | 'fractions', 131 | 'inspect', 132 | 'ast', 133 | 'statvfs', 134 | 'UserString', 135 | 'decimal', 136 | 'codecs', 137 | 'os2emxpath', 138 | 'hmac', 139 | 'genericpath', 140 | 'sunaudio', 141 | 'Queue', 142 | 'sched', 143 | 'cgitb', 144 | 'mailcap', 145 | 'pipes', 146 | 'smtplib', 147 | 'doctest', 148 | 'xmllib', 149 | 'numbers', 150 | 'os', 151 | 'dummy_thread', 152 | 'code', 153 | 'sre_constants', 154 | 'colorsys', 155 | 'modulefinder', 156 | 'bdb', 157 | 'calendar', 158 | 'urllib', 159 | 'abc', 160 | 'SocketServer', 161 | 'UserList', 162 | 'dircache', 163 | 'pydoc', 164 | 'weakref', 165 | 'ConfigParser', 166 | 'pickletools', 167 | 're', 168 | 'posixpath', 169 | 'chunk', 170 | 'mimify', 171 | 'binhex', 172 | 'tabnanny', 173 | 'rlcompleter', 174 | 'runpy', 175 | 'pyclbr', 176 | 'stringprep', 177 | 'glob', 178 | 'nntplib', 179 | 'popen2', 180 | 'formatter', 181 | 'functools', 182 | 'symtable', 183 | 'repr', 184 | 'smtpd', 185 | 'macpath', 186 | 'pickle', 187 | 'sets', 188 | 'string', 189 | 'urllib2', 190 | 'shutil', 191 | 'shlex', 192 | 'xdrlib', 193 | 'sysconfig', 194 | 'profile', 195 | 'gzip', 196 | 'tokenize', 197 | 'robotparser', 198 | 'socket', 199 | 'difflib', 200 | 'UserDict', 201 | 'platform', 202 | 'argparse', 203 | 'plistlib', 204 | 'cmd', 205 | 'nturl2path', 206 | 'locale', 207 | 'tempfile', 208 | 'random', 209 | 'toaiff', 210 | 'webbrowser', 211 | 'SimpleHTTPServer', 212 | 'collections', 213 | 'bisect', 214 | 'symbol', 215 | 'htmlentitydefs', 216 | 'io', 217 | 'whichdb', 218 | 'traceback', 219 | 'filecmp', 220 | 'compileall', 221 | 'ntpath', 222 | 'codeop', 223 | 'StringIO', 224 | 'poplib', 225 | 'email', 226 | 'json', 227 | 'multiprocessing', 228 | 'hotshot', 229 | 'xml', 230 | 'unittest', 231 | 'lib2to3', 232 | 'encodings', 233 | 'distutils', 234 | 'idlelib', 235 | 'wsgiref', 236 | 'curses', 237 | 'importlib', 238 | 'compiler', 239 | 'sqlite3', 240 | 'ctypes', 241 | 'logging' 242 | ] 243 | -------------------------------------------------------------------------------- /subpy/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdiehl/subpy/60e2bb9c9e7f542a121999cc324c4569ff2d23b2/subpy/tests/__init__.py -------------------------------------------------------------------------------- /subpy/tests/test_features.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import importlib 4 | 5 | is_py3k = bool(sys.version_info[0] == 3) 6 | 7 | from subpy import detect 8 | from subpy import features as f 9 | 10 | tests = [] 11 | 12 | #------------------------------------------------------------------------ 13 | 14 | class TestFeatureDetect(unittest.TestCase): 15 | 16 | def has(self, feature, fn): 17 | self.assertTrue(feature in detect(fn)) 18 | 19 | def not_has(self, feature, fn): 20 | self.assertTrue(feature not in detect(fn)) 21 | 22 | def test_implicit_casts(self): 23 | 24 | def fn(): 25 | return 1+2.0 26 | 27 | self.has(f.ImplicitCasts, fn) 28 | 29 | def test_generators(self): 30 | 31 | def fn(): 32 | yield 1 33 | 34 | self.has(f.Generators, fn) 35 | 36 | def test_delvar(self): 37 | 38 | def fn(): 39 | x = 3 40 | del x 41 | 42 | self.has(f.DelVar, fn) 43 | 44 | def test_closure(self): 45 | 46 | def fn(): 47 | x = 3 48 | def closure(): 49 | print(x) 50 | 51 | self.has(f.Closures, fn) 52 | 53 | def test_classes(self): 54 | 55 | def fn(): 56 | class Class: 57 | pass 58 | 59 | self.has(f.Classes, fn) 60 | 61 | def test_decorators(self): 62 | 63 | def fn(): 64 | @f 65 | def g(): 66 | pass 67 | 68 | self.has(f.Decorators, fn) 69 | 70 | def test_varargs1(self): 71 | 72 | def fn(): 73 | def foo(*xs): 74 | pass 75 | 76 | self.has(f.VarArgs, fn) 77 | 78 | def test_varargs2(self): 79 | 80 | def fn(): 81 | list(map(id, *xs)) 82 | 83 | self.has(f.VarArgs, fn) 84 | 85 | def test_kwargs1(self): 86 | 87 | def fn(): 88 | def foo(**kw): 89 | pass 90 | 91 | self.has(f.KeywordArgs, fn) 92 | 93 | def test_kwargs2(self): 94 | 95 | def fn(): 96 | foo(x=3) 97 | 98 | self.has(f.KeywordArgs, fn) 99 | 100 | def test_kwargs3(self): 101 | 102 | def fn(): 103 | foo(**kw) 104 | 105 | self.has(f.KeywordArgs, fn) 106 | 107 | def test_inheritance(self): 108 | 109 | def fn(): 110 | class foo(int): 111 | pass 112 | 113 | self.has(f.Inheritance, fn) 114 | self.not_has(f.MInheritance, fn) 115 | 116 | def test_minheritance(self): 117 | 118 | def fn(): 119 | class foo(int, bool): 120 | pass 121 | 122 | self.has(f.MInheritance, fn) 123 | self.has(f.Inheritance, fn) 124 | 125 | def test_class_decorators(self): 126 | 127 | def fn(): 128 | @foo 129 | class foo(object): 130 | pass 131 | 132 | self.has(f.ClassDecorators, fn) 133 | 134 | def test_assertions(self): 135 | 136 | def fn(): 137 | assert True 138 | 139 | self.has(f.Assertions, fn) 140 | 141 | def test_chain_comparisons(self): 142 | 143 | def fn(): 144 | x > y > z 145 | 146 | self.has(f.ChainComparison, fn) 147 | 148 | def test_exceptions1(self): 149 | 150 | def fn(): 151 | raise Exception 152 | 153 | self.has(f.Exceptions, fn) 154 | 155 | def test_exceptions2(self): 156 | 157 | def fn(): 158 | try: 159 | foo() 160 | except Exception as e: 161 | pass 162 | 163 | self.has(f.Exceptions, fn) 164 | 165 | def test_exceptions3(self): 166 | 167 | def fn(): 168 | try: 169 | foo() 170 | finally: 171 | pass 172 | 173 | self.has(f.Exceptions, fn) 174 | 175 | def test_lambda(self): 176 | 177 | def fn(): 178 | S = lambda x: lambda y: lambda z: x(z)(y(z)) 179 | K = lambda x: lambda y: x 180 | I = lambda x: x 181 | 182 | self.has(f.Lambda, fn) 183 | 184 | def test_relative_imports(self): 185 | 186 | def fn(): 187 | from . import foo 188 | 189 | self.has(f.RelativeImports, fn) 190 | 191 | def test_importstar(self): 192 | 193 | fn = """from sys import *""" 194 | 195 | self.has(f.ImportStar, fn) 196 | 197 | def test_heterolist(self): 198 | 199 | def fn(): 200 | [1, 2.0, None, 'guido'] 201 | 202 | self.has(f.HeteroList, fn) 203 | 204 | def test_continue(self): 205 | 206 | def fn(): 207 | while True: 208 | continue 209 | 210 | self.has(f.Continue, fn) 211 | 212 | def test_multiple_return(self): 213 | 214 | def fn(): 215 | return 1, None 216 | 217 | self.has(f.MultipleReturn, fn) 218 | 219 | def test_dict_comp(self): 220 | 221 | def fn(): 222 | {a : b for a,b in xs} 223 | 224 | self.has(f.DictComp, fn) 225 | 226 | def test_ellipsis1(self): 227 | 228 | def fn(): 229 | xs[1:2, ..., 0] 230 | 231 | self.has(f.Ellipsi, fn) 232 | 233 | def test_ellipsis2(self): 234 | 235 | def fn(): 236 | xs[...] 237 | 238 | self.has(f.Ellipsi, fn) 239 | 240 | def test_tuple_unpacking(self): 241 | 242 | def fn(): 243 | x, y = [1,2] 244 | 245 | self.has(f.TupleUnpacking, fn) 246 | 247 | def test_exec(self): 248 | 249 | def fn(): 250 | exec('foo', {}) 251 | 252 | self.has(f.Exec, fn) 253 | 254 | def test_fancy_indexing(self): 255 | 256 | def fn(): 257 | A[1:, 20:10:-2, ...] 258 | 259 | self.has(f.FancyIndexing, fn) 260 | 261 | def test_globals(self): 262 | 263 | def fn(): 264 | global x 265 | 266 | self.has(f.Globals, fn) 267 | 268 | def test_context_managers(self): 269 | 270 | def fn(): 271 | with foo: 272 | pass 273 | 274 | self.has(f.ContextManagers, fn) 275 | 276 | def test_generator_exp1(self): 277 | 278 | def fn(): 279 | (a for a in x) 280 | 281 | self.has(f.GeneratorExp, fn) 282 | 283 | def test_generator_exp2(self): 284 | 285 | def fn(): 286 | f(a for a in x) 287 | 288 | self.has(f.GeneratorExp, fn) 289 | 290 | def test_ternary(self): 291 | 292 | def fn(): 293 | a if True else b 294 | 295 | self.has(f.Ternary, fn) 296 | 297 | def test_listcomp(self): 298 | 299 | def fn(): 300 | [a for a in x] 301 | 302 | self.has(f.ListComp, fn) 303 | 304 | def test_setcomp(self): 305 | 306 | def fn(): 307 | {a for a in x} 308 | 309 | self.has(f.SetComp, fn) 310 | 311 | def test_custom_iterators1(self): 312 | 313 | def fn(): 314 | for a in foo: 315 | pass 316 | 317 | self.has(f.CustomIterators, fn) 318 | 319 | def test_custom_iterators2(self): 320 | 321 | def fn(): 322 | for a in set([1,2,3]): 323 | pass 324 | 325 | self.has(f.CustomIterators, fn) 326 | 327 | def test_custom_iterators3(self): 328 | 329 | def fn(): 330 | for a in range(25): 331 | pass 332 | for a in range(25): 333 | pass 334 | 335 | self.not_has(f.CustomIterators, fn) 336 | 337 | def test_printing(self): 338 | 339 | def fn(): 340 | print('hello world') 341 | 342 | self.has(f.Printing, fn) 343 | 344 | def test_metaclass(self): 345 | 346 | def fn(): 347 | class Foo(object, metaclass=Bar): 348 | pass 349 | 350 | self.has(f.Metaclasses, fn) 351 | 352 | tests.append(TestFeatureDetect) 353 | 354 | #------------------------------------------------------------------------ 355 | 356 | class TestStandardLibrary(unittest.TestCase): 357 | 358 | def test_fullstdlib(self): 359 | from subpy.stdlib import libraries 360 | 361 | for lib in libraries: 362 | mod = importlib.import_module(lib) 363 | detect(mod) 364 | 365 | tests.append(TestStandardLibrary) 366 | 367 | #------------------------------------------------------------------------ 368 | 369 | class TestToplevel(unittest.TestCase): 370 | 371 | def test_detect(self): 372 | from subpy import detect, features 373 | 374 | def foo(): 375 | lambda x:x 376 | x, y = (1,2) 377 | 378 | t1 = features.TupleUnpacking in detect(foo) 379 | t2 = features.Lambda in detect(foo) 380 | 381 | self.assertTrue(t1) 382 | self.assertTrue(t2) 383 | 384 | def test_checker(self): 385 | from subpy import checker 386 | from subpy.features import ListComp, SetComp 387 | 388 | def comps(): 389 | return [x**2 for x in range(25)] 390 | 391 | my_subset = set([ 392 | ListComp, 393 | SetComp, 394 | ]) 395 | 396 | features = checker(comps) 397 | 398 | self.assertTrue(ListComp in features) 399 | 400 | def test_validator(self): 401 | 402 | from subpy import validator, FullPython, FeatureNotSupported 403 | from subpy.features import ListComp, SetComp 404 | 405 | def comps(): 406 | return [x**2 for x in range(25)] 407 | 408 | my_features = FullPython - set([ 409 | ListComp, 410 | SetComp, 411 | ]) 412 | 413 | with self.assertRaises(FeatureNotSupported): 414 | validator(comps, features=my_features) 415 | 416 | tests.append(TestToplevel) 417 | 418 | #------------------------------------------------------------------------ 419 | 420 | def run(verbosity=1, repeat=1): 421 | suite = unittest.TestSuite() 422 | for cls in tests: 423 | for _ in range(repeat): 424 | suite.addTest(unittest.makeSuite(cls)) 425 | 426 | runner = unittest.TextTestRunner(verbosity=verbosity) 427 | return runner.run(suite) 428 | 429 | if __name__ == '__main__': 430 | run() 431 | -------------------------------------------------------------------------------- /subpy/validate.py: -------------------------------------------------------------------------------- 1 | import re 2 | import ast 3 | import types 4 | import inspect 5 | from textwrap import dedent 6 | from collections import deque, defaultdict 7 | 8 | from .features import * 9 | 10 | FullPython = set([ 11 | ImplicitCasts, 12 | Generators, 13 | DelVar, 14 | Closures, 15 | Classes, 16 | Decorators, 17 | VarArgs, 18 | KeywordArgs, 19 | Inheritance, 20 | MInheritance, 21 | ClassDecorators, 22 | Assertions, 23 | ChainComparison, 24 | Exceptions, 25 | Lambda, 26 | RelativeImports, 27 | ImportStar, 28 | HeteroList, 29 | Continue, 30 | MultipleReturn, 31 | DictComp, 32 | Ellipsi, 33 | TupleUnpacking, 34 | Exec, 35 | FancyIndexing, 36 | Globals, 37 | ContextManagers, 38 | GeneratorExp, 39 | Ternary, 40 | ListComp, 41 | SetComp, 42 | CustomIterators, 43 | Printing, 44 | Metaclasses 45 | ]) 46 | 47 | def _compile_lib_matcher(libs): 48 | matches = [] 49 | for allowed in libs: 50 | matches.append(allowed.replace('.', '\\.')\ 51 | .replace('*', '.*$')) 52 | return r'|'.join(matches) 53 | 54 | #------------------------------------------------------------------------ 55 | # AST Traversal 56 | #------------------------------------------------------------------------ 57 | 58 | GLOBAL = 0 59 | 60 | class PythonVisitor(ast.NodeVisitor): 61 | 62 | def __init__(self, features, libs): 63 | self.scope = deque([('global', 0)]) 64 | self.features = features 65 | 66 | if libs: 67 | self.libs = _compile_lib_matcher(libs) 68 | else: 69 | self.libs = None 70 | 71 | def __call__(self, source): 72 | if isinstance(source, types.ModuleType): 73 | source = dedent(inspect.getsource(source)) 74 | if isinstance(source, types.FunctionType): 75 | source = dedent(inspect.getsource(source)) 76 | if isinstance(source, types.LambdaType): 77 | source = dedent(inspect.getsource(source)) 78 | elif isinstance(source, str): 79 | source = source 80 | else: 81 | raise NotImplementedError 82 | 83 | self._source = source 84 | self._ast = ast.parse(source) 85 | self.visit(self._ast) 86 | 87 | def nolib(self, node, library): 88 | #print 'NO SUPPORT! %s' % library 89 | #print self._source.split('\n')[node.lineno-1] 90 | raise SystemExit() 91 | 92 | def action(self, node, feature): 93 | raise NotImplementedError 94 | 95 | # ------------------------------------------------- 96 | 97 | def visit_comprehension(self, node): 98 | if node.ifs: 99 | ifs = list(map(self.visit, node.ifs)) 100 | target = self.visit(node.target) 101 | iter = self.visit(node.iter) 102 | 103 | def visit_keyword(self, node): 104 | value = self.visit(node.value) 105 | 106 | def check_arguments(self, node): 107 | args = node.args 108 | 109 | ### Check for variadic arguments 110 | if VarArgs not in self.features: 111 | if args.vararg: 112 | self.action(node, VarArgs) 113 | 114 | ### Check for keyword arguments 115 | if KeywordArgs not in self.features: 116 | if args.kwarg: 117 | self.action(node, KeywordArgs) 118 | if args.defaults: 119 | self.action(node, KeywordArgs) 120 | 121 | # ------------------------------------------------- 122 | 123 | def visit_Assert(self, node): 124 | ## Check for assertions 125 | if Assertions not in self.features: 126 | self.action(node, Assertions) 127 | 128 | test = self.visit(node.test) 129 | 130 | def visit_Assign(self, node): 131 | targets = node.targets 132 | 133 | ## Check for tuple unpacking 134 | if TupleUnpacking not in self.features: 135 | if len(node.targets) > 1: 136 | self.action(node, TupleUnpacking) 137 | 138 | if any(isinstance(x, ast.Tuple) for x in node.targets): 139 | self.action(node, TupleUnpacking) 140 | 141 | for target in targets: 142 | self.visit(target) 143 | self.visit(node.value) 144 | 145 | ## Check for metaclasses 146 | if Metaclasses not in self.features: 147 | if isinstance(target, ast.Name) and target.id == '__metaclass__': 148 | self.action(node, Metaclasses) 149 | 150 | def visit_Attribute(self, node): 151 | value = self.visit(node.value) 152 | 153 | def visit_AugAssign(self, node): 154 | target = self.visit(node.target) 155 | value = self.visit(node.value) 156 | op = node.op.__class__ 157 | 158 | def visit_BinOp(self, node): 159 | lhs = self.visit(node.left) 160 | rhs = self.visit(node.right) 161 | op_str = node.op.__class__ 162 | 163 | ## Check for implicit coercions between numeric types 164 | if ImplicitCasts not in self.features: 165 | if isinstance(node.left, ast.Num) and isinstance(node.right, ast.Num): 166 | a = node.left.n 167 | b = node.right.n 168 | if type(a) != type(b) and ImplicitCasts not in self.features: 169 | self.action(node, ImplicitCasts) 170 | 171 | def visit_Break(self, node): 172 | pass 173 | 174 | def visit_BoolOp(self, node): 175 | operands = list(map(self.visit, node.values)) 176 | operator = node.op.__class__ 177 | 178 | ## Check for implicit coercions between numeric types 179 | if ImplicitCasts not in self.features: 180 | for operand in node.values: 181 | if isinstance(operand, ast.Num): 182 | self.action(node, ImplicitCasts) 183 | 184 | # PY3 185 | def visit_Bytes(self, node): 186 | pass 187 | 188 | def visit_Call(self, node): 189 | name = self.visit(node.func) 190 | args = list(map(self.visit, node.args)) 191 | keywords = list(map(self.visit, node.keywords)) 192 | 193 | # Python 2.x - 3.4 194 | if hasattr(node,"starargs") and node.starargs: 195 | ## Check for variadic arguments 196 | starargs = self.visit(node.starargs) 197 | 198 | if VarArgs not in self.features: 199 | self.action(node, VarArgs) 200 | 201 | if (hasattr(node,'keywords') and node.keywords) or \ 202 | (hasattr(node,'kwargs') and node.kwargs): 203 | ## Check for keyword arguments 204 | kwargs = list(map(self.visit, node.keywords)) 205 | 206 | if KeywordArgs not in self.features: 207 | self.action(node, KeywordArgs) 208 | 209 | # Python 3.5+ 210 | # TODO 211 | 212 | 213 | def visit_ClassDef(self, node): 214 | 215 | if Classes not in self.features: 216 | self.action(node, Classes) 217 | 218 | if node.bases: 219 | bases = list(map(self.visit, node.bases)) 220 | 221 | ## Check for single inheritance 222 | if len(bases) >= 1 and Inheritance not in self.features: 223 | self.action(node, Inheritance) 224 | 225 | ## Check for multiple inheritance 226 | if len(bases) > 1 and MInheritance not in self.features: 227 | self.action(node, MInheritance) 228 | 229 | if node.decorator_list: 230 | decorators = list(map(self.visit, node.decorator_list)) 231 | 232 | ## Check for class decorators 233 | if decorators and ClassDecorators not in self.features: 234 | self.action(node, ClassDecorators) 235 | 236 | 237 | self.scope.append(('class', node)) 238 | body = list(map(self.visit, node.body)) 239 | self.scope.pop() 240 | 241 | def visit_Compare(self, node): 242 | ## Check for chained comparisons 243 | if len(node.comparators) > 1 and ChainComparison not in self.features: 244 | self.action(node, ChainComparison) 245 | 246 | operands = list(map(self.visit, [node.left] + node.comparators)) 247 | operators = [op.__class__ for op in node.ops] 248 | 249 | def visit_Continue(self, node): 250 | ## Check for continue 251 | if Continue not in self.features: 252 | self.action(node, Continue) 253 | 254 | def visit_Delete(self, node): 255 | target = list(map(self.visit, node.targets)) 256 | if DelVar not in self.features: 257 | self.action(node, DelVar) 258 | 259 | def visit_Dict(self, node): 260 | keys = list(map(self.visit, node.keys)) 261 | values = list(map(self.visit, node.values)) 262 | 263 | def visit_DictComp(self, node): 264 | key = self.visit(node.key) 265 | value = self.visit(node.value) 266 | gens = list(map(self.visit, node.generators)) 267 | 268 | ## Check for dictionary comprehensions 269 | if DictComp not in self.features: 270 | self.action(node, DictComp) 271 | 272 | def visit_Ellipsis(self, node): 273 | pass 274 | 275 | def visit_ExtSlice(self, node): 276 | list(map(self.visit, node.dims)) 277 | 278 | def visit_ExceptHandler(self, node): 279 | 280 | if node.name: 281 | name = node.name.id 282 | #if node.type: 283 | #type = node.type.id 284 | body = list(map(self.visit, node.body)) 285 | 286 | if Exceptions not in self.features: 287 | self.action(node, Exceptions) 288 | 289 | def visit_Exec(self, node): 290 | ## Check for dynamic exec 291 | if Exec not in self.features: 292 | self.action(node, Exec) 293 | 294 | body = self.visit(node.body) 295 | if node.globals: 296 | globals = self.visit(node.globals) 297 | if node.locals: 298 | locals = self.visit(node.locals) 299 | 300 | def visit_Expr(self, node): 301 | value = self.visit(node.value) 302 | 303 | def visit_For(self, node): 304 | target = self.visit(node.target) 305 | iter = self.visit(node.iter) 306 | body = list(map(self.visit, node.body)) 307 | orelse = list(map(self.visit, node.orelse)) 308 | 309 | ## Check for custom iterators 310 | if CustomIterators not in self.features: 311 | if not isinstance(node.iter, ast.Call): 312 | self.action(node, CustomIterators) 313 | elif isinstance(node.iter.func, ast.Name): 314 | if node.iter.func.id not in ['xrange', 'range']: 315 | self.action(node, CustomIterators) 316 | else: 317 | self.action(node, CustomIterators) 318 | 319 | ## Check for tuple unpacking 320 | if TupleUnpacking not in self.features: 321 | if isinstance(node.target, ast.Tuple): 322 | self.action(node.target, TupleUnpacking) 323 | 324 | def visit_FunctionDef(self, node): 325 | self.check_arguments(node) 326 | 327 | if node.decorator_list: 328 | decorators = list(map(self.visit, node.decorator_list)) 329 | 330 | ## Check for class decorators 331 | if decorators and Decorators not in self.features: 332 | self.action(node, Decorators) 333 | 334 | ## Check for closures 335 | scope_ty, scope = self.scope[-1] 336 | 337 | if scope_ty == 'function' and scope != GLOBAL: 338 | if Closures not in self.features: 339 | self.action(node, Closures) 340 | 341 | self.scope.append(('function', node)) 342 | 343 | for defn in node.body: 344 | self.visit(defn) 345 | self.scope.pop() 346 | 347 | def visit_Global(self, node): 348 | ## Check for globals 349 | if Globals not in self.features: 350 | self.action(node, Globals) 351 | 352 | def visit_GeneratorExp(self, node): 353 | self.visit(node.elt) 354 | list(map(self.visit, node.generators)) 355 | 356 | ## Check for dictionary comprehensions 357 | if GeneratorExp not in self.features: 358 | self.action(node, GeneratorExp) 359 | 360 | def visit_If(self, node): 361 | test = self.visit(node.test) 362 | body = list(map(self.visit, node.body)) 363 | orelse = list(map(self.visit, node.orelse)) 364 | 365 | def visit_IfExp(self, node): 366 | test = self.visit(node.test) 367 | body = self.visit(node.body) 368 | orelse = self.visit(node.orelse) 369 | 370 | if Ternary not in self.features: 371 | self.action(node, Ternary) 372 | 373 | def visit_Import(self, node): 374 | matcher = self.libs 375 | 376 | ## Check for unsupported libraries 377 | def check_import(name): 378 | if name.startswith('.') and RelativeImports not in self.features: 379 | self.action(node, RelativeImports) 380 | 381 | if not re.match(matcher, name): 382 | self.nolib(node, name) 383 | 384 | for package in node.names: 385 | if self.libs: 386 | check_import(package.name) 387 | 388 | def visit_ImportFrom(self, node): 389 | matcher = self.libs 390 | 391 | ## Check for unsupported libraries 392 | def check_import(name): 393 | 394 | if not re.match(matcher, name): 395 | self.nolib(node, name) 396 | 397 | for package in node.names: 398 | ## Check for "import *" 399 | if package.name == '*' and ImportStar not in self.features: 400 | self.action(node, ImportStar) 401 | 402 | if node.module == None: 403 | if RelativeImports not in self.features: 404 | self.action(node, RelativeImports) 405 | 406 | if node.module and self.libs: 407 | munged = node.module + '.' + package.name 408 | check_import(munged) 409 | 410 | def visit_Index(self, node): 411 | self.visit(node.value) 412 | 413 | def visit_Lambda(self, node): 414 | ## Check for lambdas 415 | if Lambda not in self.features: 416 | self.action(node, Lambda) 417 | 418 | self.check_arguments(node) 419 | 420 | #args = self.visit(node.args) 421 | body = self.visit(node.body) 422 | 423 | def visit_List(self, node): 424 | elts = list(map(self.visit, node.elts)) 425 | 426 | ## Check for hetereogenous lists 427 | if node.elts and not HeteroList in self.features: 428 | ty = type(node.elts[0]) 429 | for el in node.elts[1:]: 430 | if type(el) != ty: 431 | self.action(node, HeteroList) 432 | 433 | def visit_ListComp(self, node): 434 | elt = self.visit(node.elt) 435 | gens = list(map(self.visit, node.generators)) 436 | 437 | ## Check for list comprehensions 438 | if ListComp not in self.features: 439 | self.action(node, ListComp) 440 | 441 | def visit_Name(self, node): 442 | pass 443 | 444 | def visit_Num(self, node): 445 | pass 446 | 447 | def visit_Module(self, node): 448 | body = list(map(self.visit, node.body)) 449 | 450 | def visit_Pass(self, node): 451 | pass 452 | 453 | def visit_Print(self, node): 454 | ## Check for printing 455 | if Printing not in self.features: 456 | self.action(node, Printing) 457 | 458 | if node.dest: 459 | dest = self.visit(node.dest) 460 | 461 | values = list(map(self.visit, node.values)) 462 | 463 | def visit_Raise(self, node): 464 | if node.type: 465 | self.visit(node.type) 466 | 467 | ## Check for exceptions 468 | if Exceptions not in self.features: 469 | self.action(node, Exceptions) 470 | 471 | def visit_Return(self, node): 472 | if node.value: 473 | self.visit(node.value) 474 | 475 | ## Check for multiple returns 476 | if isinstance(node.value, ast.Tuple) and MultipleReturn not in self.features: 477 | self.action(node, MultipleReturn) 478 | 479 | def visit_Set(self, node): 480 | elts = list(map(self.visit, node.elts)) 481 | 482 | def visit_SetComp(self, node): 483 | elt = self.visit(node.elt) 484 | gens = list(map(self.visit, node.generators)) 485 | 486 | ## Check for set comprehensions 487 | if SetComp not in self.features: 488 | self.action(node, SetComp) 489 | 490 | def visit_Slice(self, node): 491 | lower = node.lower 492 | if lower is not None: 493 | lower = self.visit(lower) 494 | 495 | upper = node.upper 496 | if upper is not None: 497 | upper = self.visit(upper) 498 | 499 | step = node.step 500 | if step is not None: 501 | step = self.visit(step) 502 | 503 | def visit_Str(self, node): 504 | pass 505 | 506 | def visit_Starred(self, node): 507 | self.visit(node.value) 508 | 509 | def visit_Subscript(self, node): 510 | value = self.visit(node.value) 511 | slice = self.visit(node.slice) 512 | 513 | ## Check for fancy indexing 514 | if isinstance(node.slice, ast.ExtSlice) and FancyIndexing not in self.features: 515 | self.action(node, FancyIndexing) 516 | 517 | ## Check for ellipsis 518 | if Ellipsi not in self.features: 519 | if any(type(a) == ast.Ellipsis for a in node.slice.dims): 520 | self.action(node, Ellipsi) 521 | 522 | if isinstance(node.slice, ast.Ellipsis) and Ellipsi not in self.features: 523 | self.action(node, Ellipsi) 524 | 525 | def visit_TryExcept(self, node): 526 | body = list(map(self.visit, node.body)) 527 | if node.handlers: 528 | handlers = list(map(self.visit, node.handlers)) 529 | 530 | if node.orelse: 531 | orelse = list(map(self.visit, node.orelse)) 532 | 533 | ## Check for exceptions 534 | if Exceptions not in self.features: 535 | self.action(node, Exceptions) 536 | 537 | def visit_TryFinally(self, node): 538 | body = list(map(self.visit, node.body)) 539 | finalbody = list(map(self.visit, node.finalbody)) 540 | 541 | ## Check for exceptions 542 | if Exceptions not in self.features: 543 | self.action(node, Exceptions) 544 | 545 | def visit_Tuple(self, node): 546 | return list(map(self.visit, node.elts)) 547 | 548 | def visit_UnaryOp(self, node): 549 | operand = self.visit(node.operand) 550 | op = node.op.__class__ 551 | 552 | def visit_While(self, node): 553 | test = self.visit(node.test) 554 | body = list(map(self.visit, node.body)) 555 | if node.orelse: 556 | orelse = list(map(self.visit, node.orelse)) 557 | 558 | def visit_With(self, node): 559 | ## Check for context managers 560 | if ContextManagers not in self.features: 561 | self.action(node, ContextManagers) 562 | 563 | exp = self.visit(node.context_expr) 564 | if node.optional_vars: 565 | var = node.optional_vars and self.visit(node.optional_vars) 566 | body = list(map(self.visit, node.body)) 567 | 568 | def visit_Yield(self, node): 569 | ## Check for generators 570 | if Generators not in self.features: 571 | self.action(node, Generators) 572 | 573 | if node.value: 574 | value = self.visit(node.value) 575 | 576 | # PY3 577 | def visit_YieldFrom(self, node): 578 | if Generators not in self.features: 579 | self.action(node, Generators) 580 | 581 | if node.value: 582 | value = self.visit(node.value) 583 | 584 | def generic_visit(self, node): 585 | assert 0 586 | 587 | #------------------------------------------------------------------------ 588 | # Validator 589 | #------------------------------------------------------------------------ 590 | 591 | class FeatureNotSupported(SyntaxError): pass 592 | 593 | class Validator(PythonVisitor): 594 | """ Check if the given source conforms to the feature set or 595 | raise an Exception. """ 596 | 597 | def action(self, node, feature): 598 | line = self._source.splitlines()[node.lineno-1] 599 | lineno = node.lineno 600 | offset = node.col_offset 601 | raise FeatureNotSupported(feature, ('', lineno, offset + 1, line)) 602 | 603 | class Checker(PythonVisitor): 604 | """ Aggregate sites for features that don't conform to the 605 | given feature set """ 606 | 607 | def __init__(self, features, libraries): 608 | super(Checker, self).__init__(features, libraries) 609 | self.detected = None 610 | 611 | def action(self, node, feature): 612 | self.detected[feature].append(node.lineno) 613 | 614 | def __call__(self, source): 615 | self.detected = defaultdict(list) 616 | super(Checker, self).__call__(source) 617 | return dict(self.detected) 618 | 619 | class Detect(PythonVisitor): 620 | """ Aggregate sites for conform to the given feature set """ 621 | 622 | def __init__(self): 623 | super(Detect, self).__init__(set(), []) 624 | self.detected = None 625 | 626 | def action(self, node, feature): 627 | self.detected[feature].append(node.lineno) 628 | 629 | def __call__(self, source): 630 | self.detected = defaultdict(list) 631 | super(Detect, self).__call__(source) 632 | return dict(self.detected) 633 | 634 | def detect(source): 635 | d = Detect() 636 | return d(source) 637 | 638 | def checker(source, features=None, libraries=None): 639 | d = Checker(features or set(), libraries or list()) 640 | return d(source) 641 | 642 | def validator(source, features=None, libraries=None): 643 | d = Validator(features or set(), libraries or list()) 644 | return d(source) 645 | 646 | fd = detect 647 | --------------------------------------------------------------------------------