├── .gitignore ├── LICENSE ├── README.md ├── oneliner.py ├── pyproject.toml ├── requirements-dev.txt └── tests └── test_pyl.py /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | *.py[cod] 3 | __pycache__/ 4 | docs/_build/ 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2024 Georgi Valkov. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | 3. Neither the name of author nor the names of its contributors may 16 | be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL GEORGI VALKOV BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Oneliner 2 | 3 | This module improves Python\'s usefulness as a tool for writing shell 4 | one-liners. A brief usage example: 5 | 6 | ``` bash 7 | # show line numbers 8 | python -m oneliner -ne '"%5d: %s" % (NR, _)' < input.txt 9 | 10 | # convert unix timestamp to something more human readable 11 | date +%s | pyl -j ' => ' -line '_, datetime.datetime.fromtimestamp(int(_))' 12 | # 1355256353 => 2012-12-11 22:05:53 13 | 14 | # triple space a file 15 | pyl -ne 'line + "\n"*2' < input.txt 16 | 17 | # double space only non-blank lines: 18 | pyl -ne '_ if re.match("^$", _) else _+"\n"' input.txt 19 | ``` 20 | 21 | # Why? 22 | 23 | Python is a wonderful general purpose scripting language with a great 24 | breadth of excellent libraries. While the simplicity of its syntax is 25 | often cited as a major strong point, I believe that the negative 26 | attitude towards any kind of *syntax magic* has prevented Python from 27 | becoming a useful tool for writing shell one-liners. When stacked 28 | against the likes of Ruby and Perl, Python is ill-equipped to be a part 29 | of a shell pipeline. Writing one-liners is possible, but their verbosity 30 | makes them impractical. This may be attributed to the following factors: 31 | 32 | 1) Lack of a *read-print-loop* command line option. Ruby and Perl 33 | provide the `-n` and `-p` switches that makes them assume the 34 | following loop around code: 35 | 36 | for line in sys.stdin: 37 | 38 | if '-p' in options: 39 | sys.stdout.write(line) 40 | 41 | 2) No *syntax magic* and no *special variables*. Ruby and Perl provide 42 | a multitude of cryptic, yet useful variables such as: 43 | 44 | $_ last input line 45 | $. current input line number 46 | $~ the match object returned by the last successful pattern match 47 | $& the string matched by last successful pattern match 48 | $3 the string matched by the 3rd group of the last successful pattern match 49 | $* sys.argv 50 | $> sys.stdout (default output file) 51 | $< sys.stdint (default input file) 52 | $; input field separator (default value to str.split()) 53 | $/ record separator (os.linesep) 54 | $$ os.getpid() 55 | 56 | Please refer to [English.rb] and [English.pm] for more information. 57 | 58 | 3) Lack of a flexible command line import mechanism. For example, Perl 59 | has the `-M` options: 60 | 61 | perl -MDigest::MD5=md5_hex => from Digest::MD5 import md5_hex 62 | perl -MDigest::MD5 => import Digest::MD5 63 | 64 | While the CPython interpreter has the `-m` switch, it is not 65 | suitable for the task at hand. For example, the following one-liner 66 | will run [random\'s] test suite, instead of printing a random 67 | number: 68 | 69 | python -m random -c "print(random.randint(10))" 70 | 71 | All these points add up to the verbosity of Python one-liners. The 72 | following example demonstrates this: 73 | 74 | ``` bash 75 | # convert a date to a unix timestamp (using strptime) 76 | ruby -rdate -pe '$_=Date.strptime($_, "%Y-%m-%d").strftime("%s")' 77 | perl -MTime::Piece -pe '$_=Time::Piece->strptime($_, "%Y-%m-%d\n")->epoch()' 78 | python -c 'import sys,datetime; [sys.stdout.write(datetime.datetime.strptime("%Y-%m-%d", i).strftime("%s") for i in sys.stdin]' 79 | ``` 80 | 81 | But why would anyone want to write one-liners in Python, given these 82 | shortcomings and the available alternatives? I believe that when doing 83 | interactive work on the shell, the first solution that comes to mind is 84 | usually good enough. If that solution is a Python one, why not use it? 85 | 86 | # How? 87 | 88 | Python comes with all the building blocks for implementing a practical 89 | method of writing one-liners. This module tries to address the issues 90 | outlined above. The command line interface is kept as close as that of 91 | Ruby and Perl as reasonable. 92 | 93 | 1) To help with the processing of input and output, *oneliner* provides 94 | the the `-n`, `-p` and `-l` command line switches. 95 | 96 | - `-n`: assume the following loop around expressions or statements 97 | (the distinction will be clarified later): 98 | 99 | for line in sys.stdin: 100 | ... 101 | 102 | - `-p`: like `-n`, but write the value of `line` to stdout at the 103 | end of each iteration: 104 | 105 | for line in sys.stdin: 106 | ... 107 | sys.stdout.write(line) 108 | 109 | - `-l`: automatic line-ending processing. Roughly equivalent to: 110 | 111 | for line in sys.stdin: 112 | line = line.strip(os.linesep) 113 | ... 114 | sys.stdout.write(line) 115 | sys.stdout.write(os.linesep) 116 | 117 | 2) Makes the following variables available in the local namespace of 118 | each one-liner: 119 | 120 | - `line`, `L`, `_`: The current input line. Unless the `-l` switch 121 | is given, the line separatator will be a part of this string. 122 | 123 | - `words`, `W`: Corresponds to the value of 124 | `re.split(delimiter, line)` where delimiter is the value of the 125 | `-d` option. Defaults to `\s+`. 126 | 127 | The `words` list will return an empty string instead of throwing 128 | an `IndexError` when a non-existent item is referenced. This 129 | behavior is similar to that of arrays in Ruby and field 130 | variables in Awk. 131 | 132 | - `NR`: Current input line number. 133 | 134 | - `FN`: Current input file name. If oneliner is processing input 135 | from stdin `FN` will be equal to ``, otherwise it 136 | corresponds to the current input file given on the command line. 137 | For example: 138 | 139 | echo example | python -m oneliner -ne '"%s:%s\t %s" % (FN, NR, L)' 140 | => :1 example 141 | 142 | python -m oneliner -ne '"%s:%s\t %s" % (FN, NR, L)' example.txt 143 | => example1.txt:1 line 1 144 | 145 | 3) Provide the `-m` and `-M` options and a mini-language for specifying 146 | imports. This is best illustrated by an example: 147 | 148 | -m os,sys,re,pickle => import os, sys, re, pickle 149 | -m os -m sys -m re => import os, sys, re 150 | -m os sys re pickle => import os, sys, re, pickle 151 | -m os.path.[*] => from os.path import * 152 | -m os.path.[join,exists] => from os.path import join, exists 153 | -m subprocess=sub => import subprocess as sub 154 | -m datetime.[datetime=dt] => from datetime import datetime as dt 155 | -M os.path => from os.path import * 156 | 157 | # Installing 158 | 159 | The latest stable version of *python-oneliner* is available on pypi and 160 | may be installed with pip. 161 | 162 | ``` bash 163 | $ python -m pip install oneliner 164 | ``` 165 | 166 | Alternatively, you may simply put the [oneline.py] file anywhere in 167 | [sys.path]{.title-ref}. 168 | 169 | # Todo 170 | 171 | - Support one-liners that don\'t deal with input/output only. If `-n` 172 | or `-p` are not given, *python-oneliner* should behave mostly like 173 | `python -c` does. 174 | - Persistent variables in statement one-liners. 175 | - The result of an expression one-liner is always written to stdout 176 | (even if `-n`). 177 | - Define the behaviour of multiple expression/statements specified on 178 | the command line. 179 | - Some means of emulating `BEGIN` and `END` (perhaps a `-b` and `-d` 180 | flag?) 181 | - Add more examples. 182 | 183 | # Similar Projects 184 | 185 | - [Pyp] 186 | - [Pyle] 187 | - [Funcpy] 188 | - [Red] 189 | - [Pyfil] 190 | 191 | # License 192 | 193 | *Python-oneliner* is released under the terms of the [Revised BSD 194 | License]. 195 | 196 | [English.rb]: https://github.com/ruby/ruby/blob/trunk/lib/English.rb 197 | [English.pm]: http://cpansearch.perl.org/src/GBARR/perl5.005_03/lib/English.pm 198 | [random\'s]: http://hg.python.org/cpython/file/16b1fde2275c/Lib/random.py#l728 199 | [oneline.py]: https://raw.githubusercontent.com/gvalkov/python-oneliner/master/oneliner.py 200 | [Pyp]: http://code.google.com/p/pyp/ 201 | [Pyle]: https://github.com/aljungberg/pyle 202 | [Funcpy]: http://www.pixelbeat.org/scripts/funcpy 203 | [Red]: https://bitbucket.org/johannestaas/red 204 | [Pyfil]: https://github.com/ninjaaron/pyfil 205 | [Revised BSD License]: https://raw.github.com/gvalkov/python-oneliner/master/LICENSE 206 | -------------------------------------------------------------------------------- /oneliner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import sys 6 | import argparse 7 | 8 | from sys import stderr, stdout, stdin 9 | 10 | __version__ = "0.3.1" 11 | 12 | 13 | usage_short = "Usage: {} [-hvnpdijl] [-m mod] [-e expr] [-s stmt] [ ...]" 14 | usage_short = usage_short.format("pyl" if "pyl" in sys.argv[0] else "python3 -m oneliner") 15 | 16 | usage = f""" 17 | {usage_short} 18 | 19 | Options: 20 | -h show this help message and exit 21 | -v show version and exit 22 | -n run statement or expression for each line of input 23 | -p like -n, but print value of 'line' every input cycle 24 | -d field delimiter regex (default: '\\s+') 25 | -i attempt to automatically import module names 26 | -j join tuples or lists before printing 27 | -l chomp newlines from input 28 | -m mod modules to import (see Importing Modules) 29 | -e expr python expression (see Execution Model) 30 | -s stmt python statement (see Execution Model) 31 | --debug enable debugging 32 | 33 | Execution Model: 34 | With the '-n' or '-p' flags, oneliner evaluates an expression or 35 | statement on every line of input. The local namespace of your code 36 | will include the following variables: 37 | 38 | line L _ => current input line 39 | words W => re.split(delimiter, line) (see '-d' option) 40 | NR => current input line number 41 | FN => current input file name (or stdin) 42 | 43 | If an expression is used, its return value is written to stdout: 44 | 45 | echo example | pyl -ne 'line.upper()' => EXAMPLE 46 | 47 | Statements must take care of output processing. If the '-p' flag is 48 | given, the value of the 'line' variable is written to stdout at the 49 | end of each iteration. 50 | 51 | echo example | pyl -ns 'print(line.upper())' => EXAMPLE\\n\\n 52 | echo example | pyl -ps 'line=line.upper()' => EXAMPLE\\n 53 | 54 | The '-e' and '-s' options cannot be mixed. 55 | 56 | If the '-j' flag is used, tuples and lists returned by expressions 57 | will be joined by a single space. Passing a value to the '-j' option 58 | sets the separator between elemnts. 59 | 60 | Importing Modules: 61 | The '-m' option imports modules into the global namespace of each 62 | evaluated expression or statement. The '-m' option can be specified 63 | multiple times. For example: 64 | 65 | -m os,sys,re,pickle => import os, sys, re, pickle 66 | -m os -m sys -m re => import os, sys, re 67 | -m os sys re pickle => import os, sys, re, pickle 68 | -m os.path.[*] => from os.path import * 69 | -m subprocess=sub => import subprocess as sub 70 | -m os.path.[join,exists] => from os.path import join, exists 71 | -m datetime.[datetime=dt] => from datetime import datetime as dt 72 | 73 | The os, sys and re modules are included by default. 74 | 75 | The '-i' flag will attempt to import all top-level module names 76 | found in an expression or statement. In the following example the 77 | 'time' module will be imported automatically: 78 | 79 | yes | pyl -j -line '(time.time(), line)' 80 | """ 81 | 82 | 83 | # Modules that are available to one-liners by default. 84 | provided_modules = ["os", "re", "sys"] 85 | 86 | 87 | class defaultlist(list): 88 | """A list that returns a default value on IndexErrors.""" 89 | 90 | def __init__(self, iterable, default=None): 91 | list.__init__(self, iterable) 92 | self._default = default 93 | 94 | def __getitem__(self, index): 95 | try: 96 | return list.__getitem__(self, index) 97 | except IndexError: 98 | return self._default 99 | 100 | 101 | def parse_args(argv, fh_in): 102 | """Parse arguments and do a basic sanity check.""" 103 | parser = argparse.ArgumentParser(add_help=False) 104 | o = parser.add_argument 105 | version = "%(prog)s version {}".format(__version__) 106 | 107 | o("-h", "--help", action="store_true") 108 | o("-v", "--version", action="version", version=version) 109 | o("-n", action="store_true", dest="readloop") 110 | o("-p", action="store_true", dest="printloop") 111 | o("-l", action="store_true", dest="chomp") 112 | o("-i", action="store_true", dest="autoimports") 113 | o("-d", default=r"\s+", dest="fssep") 114 | o("-j", nargs="?", dest="joinsep", default="", const=" ") 115 | o("-m", nargs="*", dest="mods", default=[]) 116 | o("-e", nargs=1, dest="expr", default=[]) 117 | o("-s", nargs=1, dest="stmt", default=[]) 118 | o("--debug", action="store_true") 119 | o("inputs", nargs="*", type=argparse.FileType("r")) 120 | 121 | parser.print_usage = lambda file: print(usage, file=file) 122 | opts = parser.parse_args(argv) 123 | 124 | if opts.help: 125 | print(usage, file=stderr) 126 | sys.exit(1) 127 | 128 | try: 129 | err = Exception 130 | if fh_in.closed and not opts.inputs: 131 | raise err("no input files or input on stdin") 132 | if not fh_in.closed and not fh_in.isatty() and opts.inputs: 133 | raise err("multiple input sources (stdin and command line)") 134 | if opts.expr and opts.stmt: 135 | raise err("cannot use expression and statement oneliners at the same time") 136 | if not opts.expr and not opts.stmt: 137 | raise err("error: no expression or statement specified") 138 | except Exception as e: 139 | print(usage_short, file=stderr) 140 | print("error: %s" % e, file=stderr) 141 | sys.exit(1) 142 | 143 | return opts 144 | 145 | 146 | def parse_modules_split(line): 147 | """ 148 | Split a comma separated list of module names, excluding commas 149 | between brackets. 150 | 151 | >>> parse_modules_split('sys,os,re') 152 | ['sys', 'os', 're'] 153 | 154 | >>> parse_modules_split('sys,os.path.[exists,join],re') 155 | ['sys', 'os.path.[exists,join]', 're'] 156 | """ 157 | 158 | if "[" not in line: 159 | # os,sys,re -> ['os', 'sys', 're'] 160 | mods = [i for i in re.split(r"[,\s]", line) if i] 161 | else: 162 | # sys,os.path.[join,exists] -> ['sys', 'os.path.[join,exists]'] 163 | mods = [] 164 | 165 | # Positions of matching '[.*]' pairs within line 166 | # '0[23]5[78] -> [1,2,3,4,6,7,8,9] 167 | brackets = re.finditer(r"\[[^\]]*\]", line) 168 | brackets = [range(*m.span()) for m in brackets] 169 | brackets = [j for i in brackets for j in i] 170 | 171 | commas = list(re.finditer(r",", line)) 172 | 173 | line = list(line) 174 | for m in commas: 175 | pos = m.start() 176 | if pos not in brackets: 177 | line[pos] = "$" 178 | 179 | mods = "".join(line).split("$") 180 | 181 | return mods 182 | 183 | 184 | def parse_modules(line): 185 | """Parse shorthand import statements. 186 | 187 | >>> parse_modules('os.path.[exists=e,join=j]') 188 | [(('os.path', ''), [('exists', 'e'), ('join', 'j')])] 189 | 190 | >>> parse_modules('os.path.[exists]') 191 | [(('os.path', ""), [('exists', '')])] 192 | 193 | >>> parse_modules('subprocess=sub', 'sys') 194 | [(('subprocess', 'sub'), []), (('sys', ''), [])] 195 | """ 196 | 197 | mods = parse_modules_split(line) 198 | imports = [] 199 | 200 | for mod in mods: 201 | # 'os.path.[exists,join]' -> ['os.path', ['exists', 'join']] 202 | if "[" in mod: 203 | mod, names, _ = re.split(r"\.?\[(.*)\]", mod) 204 | names = names.split(",") 205 | else: 206 | mod, names = mod, [] 207 | 208 | name_local = [] 209 | for name in names: 210 | # 'datetime=dt' -> ['datetime', '=', 'dt'] 211 | name, _, local = name.partition("=") 212 | name_local.append((name, local)) 213 | 214 | mod, _, local = mod.partition("=") 215 | 216 | imports.append(((mod, local), name_local)) 217 | 218 | return imports 219 | 220 | 221 | def safe_import(*args, **kw): 222 | try: 223 | return __import__(*args, **kw) 224 | except ImportError: 225 | pass 226 | 227 | 228 | def import_modules(imports): 229 | """Import the results of parse_modules().""" 230 | ctx = {} 231 | 232 | for module, names in imports: 233 | module, module_local = module 234 | module_local = module_local if module_local else module 235 | 236 | g, l = {}, {} 237 | 238 | if not names: 239 | ctx[module_local] = safe_import(module, g, l, [], 0) 240 | continue 241 | 242 | tnames = [i[0] for i in names] 243 | 244 | if "*" not in tnames: 245 | tmp = safe_import(module, g, l, tnames, 0) 246 | for name, name_local in names: 247 | name_local = name_local if name_local else name 248 | ctx[name_local] = getattr(tmp, name) 249 | else: 250 | tmp = safe_import(module, g, l, ["*"], 0) 251 | ctx.update(tmp.__dict__) 252 | 253 | return ctx 254 | 255 | 256 | def parse_import_modules(mods): 257 | ctx = {} 258 | for line in mods: 259 | imports = parse_modules(line) 260 | ctx.update(import_modules(imports)) 261 | 262 | return ctx 263 | 264 | 265 | def modules_in_code(expr): 266 | """Get module names used in an expression or statement.""" 267 | return re.findall(r"(?", "eval") for i in expr if i] 295 | code_stmt = [compile(i, "", "exec") for i in stmt if i] 296 | except SyntaxError as e: 297 | msg = "syntax error in: %s" 298 | print(msg % e.text.strip(), file=stderr) 299 | sys.exit(1) 300 | 301 | if opts.printloop or opts.readloop: 302 | nloop(code_expr, code_stmt, opts, ctx, fh_in, fh_out) 303 | 304 | 305 | def nloop(code_expr, code_stmt, opts, ctx, fh_in, fh_out): 306 | re_delim = re.compile(opts.fssep) 307 | joinsep = opts.joinsep 308 | hasfn = hasattr(fh_in, "filename") 309 | 310 | if code_expr: 311 | code_objects = code_expr 312 | isexpr = True 313 | else: 314 | code_objects = code_stmt 315 | isexpr = False 316 | 317 | for nr, line in enumerate(fh_in): 318 | line = line.strip(os.linesep) if opts.chomp else line 319 | words = [i.strip() for i in re_delim.split(line) if i] 320 | words = defaultlist(words, default="") 321 | 322 | eval_globals = ctx.copy() 323 | eval_locals = { 324 | "line": line, 325 | "words": words, 326 | "NR": nr + 1, 327 | "L": line, 328 | "_": line, 329 | "W": words, 330 | "FN": fh_in.filename() if hasfn else "", 331 | } 332 | 333 | for i in code_objects: 334 | res = eval(i, eval_globals, eval_locals) 335 | 336 | if res and isexpr: 337 | if joinsep and isinstance(res, (tuple, list)): 338 | res = joinsep.join(map(str, res)) 339 | eval_locals["line"] = res 340 | fh_out.write(str(res)) 341 | if opts.chomp: 342 | fh_out.write(os.linesep) 343 | 344 | if opts.printloop: 345 | res = str(eval_locals["line"]) 346 | fh_out.write(res) 347 | 348 | 349 | if __name__ == "__main__": 350 | main(sys.argv[1:], stdin, stdout) 351 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "oneliner" 7 | version = "0.3.1" 8 | description = "practical python one-liners" 9 | keywords = ["oneliner", "one-liner"] 10 | readme = "README.md" 11 | license = { file = "LICENSE" } 12 | requires-python = ">=3.6" 13 | authors = [{ name = "Georgi Valkov", email = "georgi.t.valkov@gmail.com" }] 14 | classifiers = [ 15 | "Development Status :: 3 - Alpha", 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: BSD License", 18 | "Environment :: Console", 19 | "Intended Audience :: Developers", 20 | "Intended Audience :: System Administrators", 21 | "Operating System :: POSIX :: Linux", 22 | ] 23 | 24 | [project.urls] 25 | "Homepage" = "https://github.com/gvalkov/python-oneliner" 26 | 27 | [project.scripts] 28 | "pyl" = "oneliner:main" 29 | 30 | [tool.setuptools_scm] 31 | 32 | [tool.ruff] 33 | line-length = 120 34 | 35 | [tool.bumpversion] 36 | current_version = "0.3.1" 37 | commit = true 38 | tag = true 39 | allow_dirty = true 40 | 41 | [[tool.bumpversion.files]] 42 | filename = "pyproject.toml" 43 | 44 | [[tool.bumpversion.files]] 45 | filename = "oneliner.py" 46 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | ruff 3 | bump-my-version ~= 0.17.4 4 | -------------------------------------------------------------------------------- /tests/test_pyl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pytest import mark 4 | from oneliner import * 5 | 6 | 7 | def test_module_finder(): 8 | mic = modules_in_code 9 | assert mic("write(sys.version)") == ["sys"] 10 | assert mic("abc.write sys.version.none") == ["abc", "sys"] 11 | assert mic(";sys.stdout.write();") == ["sys"] 12 | 13 | 14 | def test_module_split(): 15 | pms = parse_modules_split 16 | assert pms("os,sys,re") == ["os", "sys", "re"] 17 | assert pms("os, sys, re") == ["os", "sys", "re"] 18 | assert pms("os,sys, re,") == ["os", "sys", "re"] 19 | assert pms("os.path,sys,") == ["os.path", "sys"] 20 | 21 | assert pms("os.path.[exists]") == ["os.path.[exists]"] 22 | assert pms("os.path.[exists,join]") == ["os.path.[exists,join]"] 23 | assert pms("sys,os.path.[exists]") == ["sys", "os.path.[exists]"] 24 | assert pms("sys,os.path.[*]") == ["sys", "os.path.[*]"] 25 | assert pms("sys,os.path.[exists,join],re") == ["sys", "os.path.[exists,join]", "re"] 26 | 27 | 28 | def test_parse_modules(): 29 | pm = parse_modules 30 | assert pm("subprocess") == [(("subprocess", ""), [])] 31 | assert pm("subprocess=sub") == [(("subprocess", "sub"), [])] 32 | assert pm("sys=s,os=o") == [(("sys", "s"), []), (("os", "o"), [])] 33 | 34 | assert pm("os.path.[exists]") == [(("os.path", ""), [("exists", "")])] 35 | assert pm("os.path.[exists,join]") == [(("os.path", ""), [("exists", ""), ("join", "")])] 36 | assert pm("os.path.[exists=e,join=j]") == [(("os.path", ""), [("exists", "e"), ("join", "j")])] 37 | assert pm("sys,os.path.[*]") == [(("sys", ""), []), (("os.path", ""), [("*", "")])] 38 | 39 | 40 | def test_import_modules(): 41 | im = lambda x: sorted(import_modules(parse_modules(x))) 42 | assert im("sys,os,re") == ["os", "re", "sys"] 43 | assert im("subprocess=sub") == ["sub"] 44 | assert im("subprocess=sub,sys") == ["sub", "sys"] 45 | assert im("sys=a,os=b,re=c") == ["a", "b", "c"] 46 | 47 | assert im("os.path.[exists]") == ["exists"] 48 | assert im("sys=s,os.path.[exists]") == ["exists", "s"] 49 | assert im("os.path.[exists,join]") == ["exists", "join"] 50 | assert im("os.path.[exists=e,join=j]") == ["e", "j"] 51 | 52 | 53 | def test_import_modules_wildcard(): 54 | res = import_modules(parse_modules("os.path.[*]")) 55 | assert all([i in res for i in ["join", "isdir", "abspath"]]) 56 | 57 | 58 | def test_default_list(): 59 | l = defaultlist([1, 2, 3]) 60 | assert l[0] == 1 61 | assert l[10] == None 62 | 63 | l = defaultlist([1, 2, 3], default=True) 64 | assert l[10] == True 65 | 66 | 67 | def test_functional(): 68 | data, args = "example", ["-ne", "line.upper()"] 69 | assert run(args, data) == "EXAMPLE" 70 | 71 | data, args = "example", ["-ns", "print(line.upper())"] 72 | assert run(args, data) == "EXAMPLE\n" 73 | 74 | data = "1355256353" 75 | args = ["-j", " => ", "-line" "_, datetime.datetime.fromtimestamp(int(_))"] 76 | assert run(args, data) == "1355256353 => 2012-12-11 21:05:53\n" 77 | 78 | 79 | # ----------------------------------------------------------------------------- 80 | # utility functions 81 | try: 82 | from StringIO import StringIO 83 | except ImportError: 84 | from io import StringIO 85 | 86 | 87 | def stdinstr(s): 88 | fh = StringIO(s) 89 | fh.isatty = lambda: False 90 | return fh 91 | 92 | 93 | def run(args, data): 94 | res = StringIO() 95 | main(args, stdinstr(data), fh_out=res) 96 | return res.getvalue() 97 | --------------------------------------------------------------------------------