From df65d181e854738a2f6c209d229ae22999d09886 Mon Sep 17 00:00:00 2001 From: Brice Arnould Date: Fri, 25 Sep 2015 23:47:40 +0200 Subject: [PATCH] Adds -i/--import short import syntax. --- README.rst | 8 ++++- docs/contents.rst | 3 +- docs/examples.rst | 14 +++++++- docs/imports.rst | 38 ++++++++++++++++++++++ docs/internals.rst | 75 +++++++++++++++++++++++++++++++++++++++---- docs/man.rst | 4 ++- py1/imports.py | 68 +++++++++++++++++++++++++++++++++++++++ py1/main.py | 13 ++++++-- setup.py | 1 + tests/test_imports.py | 67 ++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 3 +- 11 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 docs/imports.rst create mode 100644 py1/imports.py create mode 100644 tests/test_imports.py diff --git a/README.rst b/README.rst index 5c86507..6656d4a 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,7 @@ Using ``{{`` ``}}`` instead of indentation, and ``;`` to separate statements: .. code-block:: bash - py1 "import sys ; if True: {{ print(sys.version) }}" + py1 "a = 1+2; if a > 4: {{ print(a) }}" The wrapper script defines a convenient set of 1&2-letters variables and functions. @@ -57,6 +57,12 @@ For example, to count lines matching ``'$a*^'``: py1 --begin "count=0" --each-line "if M('$a*^'): count += 1" --end "P(count)" +Lastly the wrapper script provide a short notation to easily import modules. + +.. code-block:: bash + + py1 --import "math/*" "P(cos(pi))" + To learn more you can read the `list of one letter functions and variables `_ or just look at diff --git a/docs/contents.rst b/docs/contents.rst index b3736da..911c13a 100644 --- a/docs/contents.rst +++ b/docs/contents.rst @@ -8,8 +8,9 @@ Structure of this website intro examples variables + imports internals - + Indices and tables ================== diff --git a/docs/examples.rst b/docs/examples.rst index d42f810..18606e9 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,6 +4,18 @@ Examples ======== +Unix timestamp +-------------- + +Formats a Unix timestamp (1443214640) as a human-readable string. + +.. code:: bash + + py1 -i 'time/ctime' 'P(ctime(1443214640))' + +To do so we import ctime from the time module and print it. + + Count blank lines ----------------- @@ -23,7 +35,7 @@ Show the second and third fields of ``/etc/passwd``, a file whose fields are sep .. code:: bash - cat /etc/passwd | py1 -b 'WS=":"' -l 'P(W[1:2])' + cat /etc/passwd | py1 -b 'WS=":"' -l 'P(W[1:2])' Here we override WS to use the "``:``" separator of ``/etc/passwd``. diff --git a/docs/imports.rst b/docs/imports.rst new file mode 100644 index 0000000..d11714c --- /dev/null +++ b/docs/imports.rst @@ -0,0 +1,38 @@ +.. highlight:: bash + + +Imports +======= + +``--i``/``--import`` is a shortcut to easily import external libraries. + +Importing a module +------------------ + +The equivalent of ``import xyz`` is ``--import xyz``. It is equivalent to ``--begin import xyz``, just shorter. + +.. code:: bash + + py1 --begin 'import math' 'P(math.cos(math.pi))' + py1 --import 'math' 'P(math.cos(math.pi))' + py1 -i 'math' 'P(math.cos(math.pi))' + +Importing specific symbols +-------------------------- + +The equivalent of ``from xyz import abc`` is ``--import xyz/abc``. You can import multiple functions with ``--import xyz/abc,def`` + +.. code:: bash + + py1 --import 'math/cos,pi' 'P(cos(pi))' + py1 -i 'math/*' 'P(cos(pi))' + + +Importing with a specific name +------------------------------ + +The equivalent of ``import abc as ABC`` is ``--import abc:ABC``. You can rename specific symbols in the same way like ``--import xyz/abc:ABC`` + +.. code:: bash + + py1 --import 'math:M' 'P(M.cos(M.pi))' diff --git a/docs/internals.rst b/docs/internals.rst index c7d7849..e314791 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1,12 +1,73 @@ +.. highlight:: bash + + Internals ========= -This describes the implementation of py1, not its usage. +Unix timestamp +-------------- + +Formats a Unix timestamp (1443214640) as a human-readable string. + +.. code:: bash + + py1 -i 'time/ctime' 'P(ctime(1443214640))' + +To do so we import ctime from the time module and print it. + + +Count blank lines +----------------- + +Count the number of blank lines in a file. + +.. code:: bash + + py1 -b 'c=0' -l 'if not L: c += 1' -e 'P(c)' + +Here we define an acumulator variable and increment it when the line satisfies a criteria. + + +Print 2nd and 3rd fields +------------------------ + +Show the second and third fields of ``/etc/passwd``, a file whose fields are separated by "``:``". + +.. code:: bash + + cat /etc/passwd | py1 -b 'WS=":"' -l 'P(W[1:2])' + +Here we override WS to use the "``:``" separator of ``/etc/passwd``. + + +Show lines matching a regexp +---------------------------- + +Show lines matching the regexp '$a+^'. + +.. code:: bash + + py1 -l 'if M("$a+^", L): P(L)' + +Here we use the M matching function to match the regexp. + +Count blank lines again +----------------------- + +Count the number of blank lines in a file. + +.. code:: bash + + py1 -e 'P(sum(1 if l else 0 for l in F))' + +Here we do not set a per-line statement and instead have sum iterate over F. + +Group by +-------- + +Given a file of '$name $value', with name being repeated, sum the values for each name. -.. autosummary:: - :toctree: _autosummary +.. code:: bash - py1.curly - py1.runner - py1.template_reader - py1.tty + py1 -b 'd=defaultdict(int)' -l 'd[W[0]] += int(W[1])' + -e 'for n, v in d: P(n, v)' diff --git a/docs/man.rst b/docs/man.rst index 199b413..73e129f 100644 --- a/docs/man.rst +++ b/docs/man.rst @@ -36,9 +36,11 @@ For more examples, please see `http://py1.vleu.net/examples.html`. For the defin Options ------- - + -h, --help show an help message and exit + -i IMPORT, --import IMPORT imports modules described in abbreviated form + -b CODE, --begin CODE code run once first -e CODE, --end CODE code run once at the end diff --git a/py1/imports.py b/py1/imports.py new file mode 100644 index 0000000..4eeb491 --- /dev/null +++ b/py1/imports.py @@ -0,0 +1,68 @@ +# Copyright (c) 2013-2015, Brice Arnould +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following condition are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Parses the import statements.""" + + +class Error(Exception): + + """Base class for errors from this module.""" + + +class BadShortSyntaxError(Error): + + """The user-provided code does not decode to valid code.""" + + def __init__(self, short_import, expanded_import): + msg ='Import statement %s means %s which is invalid.' % ( + short_import, expanded_import) + super(BadShortSyntaxError, self).__init__(msg) + self.short_import = short_import + self.expanded_import = expanded_import + + +def expand_short(short_import): + + # from_str: the (optional) part between "from" and "import" + if '/' in short_import: + from_str, imported_str = short_import.split('/', 1) + else: + from_str, imported_str = None, short_import + + expanded_imported_str = imported_str.replace(':', ' as ') + + if from_str: + res = 'from %s import %s' % (from_str, expanded_imported_str) + else: + res = 'import %s' % expanded_imported_str + + try: + code = compile(res, '', 'exec') + except SyntaxError: + code = None + + if not code: + raise BadShortSyntaxError(short_import, res) + + return res diff --git a/py1/main.py b/py1/main.py index 23ae641..c00f187 100644 --- a/py1/main.py +++ b/py1/main.py @@ -28,6 +28,7 @@ from py1 import template_reader from py1 import constants from py1 import curly +from py1 import imports from py1 import runner from py1 import tty @@ -51,6 +52,9 @@ def _get_option_parser(): metavar='PY', help='Code run each line.') p.add_argument('-e', '--end', action='append', default=[], metavar='PY', help='Code run once at the end.') + p.add_argument('-i', '--import', action='append', default=[], + dest='import_list', metavar='IMPORT', + help='Imports modules described in abbreviated form.') p.add_argument('-c', '--dump-code', '--code', default=False, # Value if not provided const=_ARG_CONCISE, # Value if provided without argument @@ -84,8 +88,11 @@ def _uncurl_list_or_die(escaped_list): def main(args=None): parser = _get_option_parser() args = parser.parse_args(args) + + begin = [imports.expand_short(i) for i in args.import_list] + if args.single_snippet: - # We expect no options if given a single snippet. + # We expect no additional code if given a single snippet. for opt in ('begin', 'end', 'each_line'): if getattr(args, opt): snippet = _abreviate(args.single_snippet) @@ -93,9 +100,9 @@ def main(args=None): '--%s is specified yet there is a lonely code snippet, try' ' fixing quotes or adding --begin/-b before: "%s"' % (opt, snippet)) - begin = [args.single_snippet] + begin.append(args.single_snippet) else: - begin = args.begin + begin += args.begin code = template_reader.build_code( begin=_uncurl_list_or_die(begin), diff --git a/setup.py b/setup.py index a7118fc..1d9b591 100755 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ class build(build.build): py_modules=[ 'py1.constants', 'py1.curly', + 'py1.imports', 'py1.main', 'py1.runner', ], diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 0000000..9ca2030 --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,67 @@ +# Copyright (c) 2013-2015, Brice Arnould +# All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following condition are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Tests for the imports module.""" + +import threading +import unittest + +from py1 import imports + + +class TestExpand(unittest.TestCase): + + def testGoodSamples(self): + samples = [ + # The full syntax + ('m1.m2.m3/i1:n1,i2:n2,i3', 'from m1.m2.m3 import i1 as n1,i2 as n2,i3'), + # No from + ('i1:n1,i2:n2,i3', 'import i1 as n1,i2 as n2,i3'), + # Just one import + ('i', 'import i'), + # Import * + ('i/*', 'from i import *'), + ] + + for short_import, expanded_import in samples: + self.assertEqual(expanded_import, imports.expand_short(short_import)) + + def testInvalidSamples(self): + samples = [ + # Extra "!" + 'i!', + # Import star without scope + '*', + # Missing name + 'a:', + # Missing module + ':b', + ] + + for short_import in samples: + self.assertRaises(imports.BadShortSyntaxError, imports.expand_short, short_import) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py index 62eb5c2..893fabc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -58,7 +58,8 @@ def testRaw(self): def testAwkLike(self): """Test running valid code in awk-like mode.""" with self.ioPatcher(): - main.main(['-b', 'import io; F=io.StringIO("85\\n12")', + main.main(['-i', 'io', + '-b', 'F=io.StringIO("85\\n12")', '-b', 'count = 0', '-l', 'count += int(L)', '-e', 'P("count=", count)',