From 07860b5ce6de5987e5cf20b86320310817e0c0ff Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 19 Apr 2013 19:20:09 -0700 Subject: [PATCH 01/12] Allow variable without value in `let' declaration Fixes issue #138 Signed-off-by: Julien Danjou --- hy/core/bootstrap.py | 11 ++++++++--- tests/native_tests/language.hy | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/hy/core/bootstrap.py b/hy/core/bootstrap.py index 50efc66..4c80a9f 100644 --- a/hy/core/bootstrap.py +++ b/hy/core/bootstrap.py @@ -133,12 +133,17 @@ def rest_macro(tree): @macro("let") def let_macro(tree): tree.pop(0) # "let" - ret = tree.pop(0) # vars + variables = tree.pop(0) # tree is now the body expr = HyExpression([HySymbol("fn"), HyList([])]) - for var in ret: - expr.append(HyExpression([HySymbol("setf"), var[0], var[1]])) + for var in variables: + if isinstance(var, list): + expr.append(HyExpression([HySymbol("setf"), + var[0], var[1]])) + else: + expr.append(HyExpression([HySymbol("setf"), + var, HySymbol("None")])) for stmt in tree: expr.append(stmt) diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 1480d94..654ccf3 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -479,7 +479,8 @@ (defn test-let [] "NATIVE: test let works rightish" - (assert (= (let [[x 1] [y 2] [z 3]] (+ x y z)) 6))) + (assert (= (let [[x 1] [y 2] [z 3]] (+ x y z)) 6)) + (assert (= (let [[x 1] a [y 2] b] (if a 1 2)) 2))) (defn test-if-mangler [] From 74ea8fe5e5124a1ff2848056f591fed32ff35950 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Fri, 19 Apr 2013 18:31:32 -0700 Subject: [PATCH 02/12] Allow fn to have no body Signed-off-by: Julien Danjou --- hy/compiler.py | 2 +- tests/compilers/test_ast.py | 2 ++ tests/native_tests/language.hy | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hy/compiler.py b/hy/compiler.py index 19a68df..14c33ed 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -1027,7 +1027,7 @@ class HyASTCompiler(object): col_offset=expr.start_column) @builds("fn") - @checkargs(min=2) + @checkargs(min=1) def compile_fn_expression(self, expression): expression.pop(0) # fn diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 3b34e14..c565046 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -300,6 +300,8 @@ def test_ast_anon_fns_basics(): """ Ensure anon fns work. """ code = hy_compile(tokenize("(fn (x) (* x x))")).body[0] assert type(code) == ast.FunctionDef + code = hy_compile(tokenize("(fn (x))")).body[0] + cant_compile("(fn)") def test_ast_non_decoratable(): diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 1480d94..b902526 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -474,7 +474,9 @@ (defn test-fn-return [] "NATIVE: test function return" (setv fn-test ((fn [] (fn [] (+ 1 1))))) - (assert (= (fn-test) 2))) + (assert (= (fn-test) 2)) + (setv fn-test (fn [])) + (assert (= (fn-test) None))) (defn test-let [] From 494bf0e8adfd42d9b27290fd3d32eb8ce5e6ecca Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sat, 20 Apr 2013 16:06:32 +0200 Subject: [PATCH 03/12] Remove the import-as and import-from builtins The new and improved (import) can handle all cases import-as and import-from did, so drop the latter two from the language. To do this, the import builtin had to be changed a little: if there's a single import statement to return, return it as-is, otherwise return a list of imports. Signed-off-by: Gergely Nagy --- bin/hy | 4 ++-- bin/hyc | 2 +- docs/tutorial.rst | 2 +- eg/debian/parse-rfc822.hy | 2 +- eg/gevent/sockets/socket-server.hy | 2 +- eg/python3/futures/hello-world.hy | 7 +++---- eg/sh/tagwords.hy | 2 +- eg/sunlight/party-count.hy | 4 ++-- hy/compiler.py | 32 ++++++------------------------ tests/compilers/native/quoting.hy | 2 +- tests/compilers/test_ast.py | 9 ++------- tests/native_tests/language.hy | 6 +++--- 12 files changed, 24 insertions(+), 50 deletions(-) diff --git a/bin/hy b/bin/hy index 45405aa..28dbe3e 100755 --- a/bin/hy +++ b/bin/hy @@ -98,7 +98,7 @@ def koan_macro(tree): return HyExpression([HySymbol('print'), HyString(""" - => (import-from sh figlet) + => (import [sh [figlet]]) => (figlet "Hi, Hy!") _ _ _ _ _ _ | | | (_) | | | |_ _| | @@ -113,7 +113,7 @@ def koan_macro(tree): ;;; this one plays with command line bits -(import-from sh cat grep) +(import [sh [cat grep]]) (-> (cat "/usr/share/dict/words") (grep "-E" "bro$")) diff --git a/bin/hyc b/bin/hyc index 73c9d45..c371b4b 100755 --- a/bin/hyc +++ b/bin/hyc @@ -1,6 +1,6 @@ #!/usr/bin/env hy (import sys) -(import-from hy.importer write-hy-as-pyc) +(import [hy.importer [write-hy-as-pyc]]) (write-hy-as-pyc (get sys.argv 1)) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 584652f..14b7193 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -392,7 +392,7 @@ a pipe: .. code-block:: clj - => (import-from sh cat grep wc) + => (import [sh [cat grep wc]]) => (-> (cat "/usr/share/dict/words") (grep "-E" "^hy") (wc "-l")) 210 diff --git a/eg/debian/parse-rfc822.hy b/eg/debian/parse-rfc822.hy index b9f008f..1daaf97 100644 --- a/eg/debian/parse-rfc822.hy +++ b/eg/debian/parse-rfc822.hy @@ -35,6 +35,6 @@ (print source "is a(n)" (get block "Description")) -(import-from sh apt-cache) +(import [sh [apt-cache]]) (setv archive-block (parse-rfc822-stream (.show apt-cache source))) (print "The archive has version" (get archive-block "Version") "of" source) diff --git a/eg/gevent/sockets/socket-server.hy b/eg/gevent/sockets/socket-server.hy index 36fda86..b80289f 100644 --- a/eg/gevent/sockets/socket-server.hy +++ b/eg/gevent/sockets/socket-server.hy @@ -1,4 +1,4 @@ -(import-from gevent.server StreamServer) +(import [gevent.server [StreamServer]]) (defn handle [socket address] diff --git a/eg/python3/futures/hello-world.hy b/eg/python3/futures/hello-world.hy index 3b031bc..6382ca6 100644 --- a/eg/python3/futures/hello-world.hy +++ b/eg/python3/futures/hello-world.hy @@ -1,7 +1,6 @@ -(import-from concurrent.futures ThreadPoolExecutor as-completed) -(import-from random randint) - -(import-from sh sleep) +(import [concurrent.futures [ThreadPoolExecutor as-completed]] + [random [randint]] + [sh [sleep]]) (defn task-to-do [] (sleep (randint 1 5))) diff --git a/eg/sh/tagwords.hy b/eg/sh/tagwords.hy index 6ca5a35..16ae9bf 100644 --- a/eg/sh/tagwords.hy +++ b/eg/sh/tagwords.hy @@ -1,5 +1,5 @@ ;; python-sh from hy -(import-from sh cat grep) +(import [sh [cat grep]]) (print "Words that end with `tag`:") (print (-> (cat "/usr/share/dict/words") (grep "-E" "tag$"))) diff --git a/eg/sunlight/party-count.hy b/eg/sunlight/party-count.hy index 991bb1b..de235cb 100755 --- a/eg/sunlight/party-count.hy +++ b/eg/sunlight/party-count.hy @@ -4,8 +4,8 @@ ; the source. (import sys) -(import-from sunlight openstates) -(import-from collections Counter) +(import [sunlight [openstates]] + [collections [Counter]]) (def *state* (get sys.argv 1)) diff --git a/hy/compiler.py b/hy/compiler.py index 19a68df..07d7a14 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -567,29 +567,10 @@ class HyASTCompiler(object): raise TypeError("Unknown entry (`%s`) in the HyList" % (entry)) - return rimports - - @builds("import_as") - def compile_import_as_expression(self, expr): - expr.pop(0) # index - modlist = [expr[i:i + 2] for i in range(0, len(expr), 2)] - return ast.Import( - lineno=expr.start_line, - col_offset=expr.start_column, - module=ast_str(expr.pop(0)), - names=[ast.alias(name=ast_str(x[0]), - asname=ast_str(x[1])) for x in modlist]) - - @builds("import_from") - @checkargs(min=1) - def compile_import_from_expression(self, expr): - expr.pop(0) # index - return ast.ImportFrom( - lineno=expr.start_line, - col_offset=expr.start_column, - module=ast_str(expr.pop(0)), - names=[ast.alias(name=ast_str(x), asname=None) for x in expr], - level=0) + if len(rimports) == 1: + return rimports[0] + else: + return rimports @builds("get") @checkargs(2) @@ -1171,9 +1152,8 @@ def hy_compile(tree, root=None): imported.add(entry) imports.append(HyExpression([ - HySymbol("import_from"), - HySymbol(package), - HySymbol(entry) + HySymbol("import"), + HyList([HySymbol(package), HyList([HySymbol(entry)])]) ]).replace(replace)) _ast = compiler.compile(imports) + _ast diff --git a/tests/compilers/native/quoting.hy b/tests/compilers/native/quoting.hy index 6a1b522..477e237 100644 --- a/tests/compilers/native/quoting.hy +++ b/tests/compilers/native/quoting.hy @@ -1,7 +1,7 @@ ;;; ;;; -(import-from hy HyExpression HySymbol HyString) +(import [hy [HyExpression HySymbol HyString]]) (defn test-basic-quoting [] diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 3b34e14..0867111 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -208,13 +208,8 @@ def test_ast_bad_yield(): def test_ast_good_import_from(): - "Make sure AST can compile valid import-from" - hy_compile(tokenize("(import-from x y)")) - - -def test_ast_bad_import_from(): - "Make sure AST can't compile invalid import-from" - cant_compile("(import-from)") + "Make sure AST can compile valid selective import" + hy_compile(tokenize("(import [x [y]])")) def test_ast_good_get(): diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 1480d94..b03e6f0 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -1,8 +1,8 @@ ; -(import-from tests.resources kwtest function-with-a-dash) -(import-from os.path exists isdir isfile) -(import-as sys systest) +(import [tests.resources [kwtest function-with-a-dash]] + [os.path [exists isdir isfile]] + [sys :as systest]) (import sys) From 07e99dbd33150e57712d5df62cddc951a9eb9af3 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Sat, 20 Apr 2013 22:11:53 +0200 Subject: [PATCH 04/12] importer: doc update, MetaImport refactor Signed-off-by: Julien Danjou --- hy/importer.py | 88 +++++++++++++++++---------------- tests/importer/test_importer.py | 4 +- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/hy/importer.py b/hy/importer.py index 5406be0..41d43fc 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -42,37 +42,41 @@ else: long_type = long # NOQA -def compile_(ast, filename, mode): +def ast_compile(ast, filename, mode): + """Compile AST. + Like Python's compile, but with some special flags.""" return compile(ast, filename, mode, __future__.CO_FUTURE_DIVISION) -def import_buffer_to_hst(fd): - tree = tokenize(fd.read() + "\n") - tree = process(tree) - return tree +def import_buffer_to_hst(buf): + """Import content from buf and return an Hy AST.""" + return process(tokenize(buf + "\n")) def import_file_to_hst(fpath): - return import_buffer_to_hst(open(fpath, 'r', encoding='utf-8')) + """Import content from fpath and return an Hy AST.""" + with open(fpath, 'r', encoding='utf-8') as f: + return import_buffer_to_hst(f.read()) + + +def import_buffer_to_ast(buf): + """ Import content from buf and return a Python AST.""" + return hy_compile(import_buffer_to_hst(buf)) def import_file_to_ast(fpath): - tree = import_file_to_hst(fpath) - _ast = hy_compile(tree) - return _ast + """Import content from fpath and return a Python AST.""" + return hy_compile(import_file_to_hst(fpath)) -def import_string_to_ast(buff): - tree = import_buffer_to_hst(StringIO(buff)) - _ast = hy_compile(tree) - return _ast +def import_file_to_module(module_name, fpath): + """Import content from fpath and puts it into a Python module. - -def import_file_to_module(name, fpath): + Returns the module.""" _ast = import_file_to_ast(fpath) - mod = imp.new_module(name) + mod = imp.new_module(module_name) mod.__file__ = fpath - eval(compile_(_ast, fpath, "exec"), mod.__dict__) + eval(ast_compile(_ast, fpath, "exec"), mod.__dict__) return mod @@ -84,7 +88,7 @@ def hy_eval(hytree, namespace): foo.end_column = 0 hytree.replace(foo) _ast = hy_compile(hytree, root=ast.Expression) - return eval(compile_(_ast, "", "eval"), namespace) + return eval(ast_compile(_ast, "", "eval"), namespace) def write_hy_as_pyc(fname): @@ -96,7 +100,7 @@ def write_hy_as_pyc(fname): timestamp = long_type(st.st_mtime) _ast = import_file_to_ast(fname) - code = compile_(_ast, fname, "exec") + code = ast_compile(_ast, fname, "exec") cfile = "%s.pyc" % fname[:-len(".hy")] if sys.version_info[0] >= 3: @@ -118,7 +122,10 @@ def write_hy_as_pyc(fname): fc.write(MAGIC) -class HyFinder(object): +class MetaLoader(object): + def __init__(self, path): + self.path = path + def is_package(self, fullname): dirpath = "/".join(fullname.split(".")) for pth in sys.path: @@ -128,33 +135,20 @@ class HyFinder(object): return True return False - def find_on_path(self, fullname): - fls = ["%s/__init__.hy", "%s.hy"] - dirpath = "/".join(fullname.split(".")) - - for pth in sys.path: - pth = os.path.abspath(pth) - for fp in fls: - composed_path = fp % ("%s/%s" % (pth, dirpath)) - if os.path.exists(composed_path): - return composed_path - - -class MetaLoader(HyFinder): def load_module(self, fullname): if fullname in sys.modules: return sys.modules[fullname] - pth = self.find_on_path(fullname) - if pth is None: + if not self.path: return sys.modules[fullname] = None - mod = import_file_to_module(fullname, pth) + mod = import_file_to_module(fullname, + self.path) ispkg = self.is_package(fullname) - mod.__file__ = pth + mod.__file__ = self.path mod.__loader__ = self mod.__name__ = fullname @@ -168,12 +162,22 @@ class MetaLoader(HyFinder): return mod -class MetaImporter(HyFinder): +class MetaImporter(object): + def find_on_path(self, fullname): + fls = ["%s/__init__.hy", "%s.hy"] + dirpath = "/".join(fullname.split(".")) + + for pth in sys.path: + pth = os.path.abspath(pth) + for fp in fls: + composed_path = fp % ("%s/%s" % (pth, dirpath)) + if os.path.exists(composed_path): + return composed_path + def find_module(self, fullname, path=None): - pth = self.find_on_path(fullname) - if pth is None: - return - return MetaLoader() + path = self.find_on_path(fullname) + if path: + return MetaLoader(path) sys.meta_path.append(MetaImporter()) diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py index ce5627c..2918c65 100644 --- a/tests/importer/test_importer.py +++ b/tests/importer/test_importer.py @@ -1,4 +1,4 @@ -from hy.importer import import_file_to_module, import_string_to_ast +from hy.importer import import_file_to_module, import_buffer_to_ast import ast @@ -10,5 +10,5 @@ def test_basics(): def test_stringer(): "Make sure the basics of the importer work" - _ast = import_string_to_ast("(defn square [x] (* x x))") + _ast = import_buffer_to_ast("(defn square [x] (* x x))") assert type(_ast.body[0]) == ast.FunctionDef From 7f230fdd108f16eae33df4147377a60958d9090f Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Sat, 20 Apr 2013 22:14:08 +0200 Subject: [PATCH 05/12] importer: remove useless import Signed-off-by: Julien Danjou --- hy/importer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/hy/importer.py b/hy/importer.py index 41d43fc..548eb1f 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -34,10 +34,8 @@ import os import __future__ if sys.version_info[0] >= 3: - from io import StringIO long_type = int else: - from StringIO import StringIO # NOQA import __builtin__ long_type = long # NOQA From 3226ecc33fc25ef4d84886ad329ad1046d8088f8 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Sat, 20 Apr 2013 22:27:10 +0200 Subject: [PATCH 06/12] Fix bin/hy and add a unit test Signed-off-by: Julien Danjou --- bin/hy | 4 ++-- tests/test_bin.py | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/test_bin.py diff --git a/bin/hy b/bin/hy index 45405aa..16e92a2 100755 --- a/bin/hy +++ b/bin/hy @@ -21,7 +21,7 @@ from hy.lex.states import Idle, LexException from hy.lex.machine import Machine from hy.compiler import hy_compile from hy.core import process -from hy.importer import compile_ +from hy.importer import ast_compile import hy.completer @@ -59,7 +59,7 @@ class HyREPL(code.InteractiveConsole): _machine = Machine(Idle, 1, 0) try: _ast = hy_compile(tokens, root=ast.Interactive) - code = compile_(_ast, filename, symbol) + code = ast_compile(_ast, filename, symbol) except Exception: self.showtraceback() return False diff --git a/tests/test_bin.py b/tests/test_bin.py new file mode 100644 index 0000000..ff05516 --- /dev/null +++ b/tests/test_bin.py @@ -0,0 +1,8 @@ +import subprocess + + +def test_bin_hy(): + p = subprocess.Popen("echo | bin/hy", + shell=True) + p.wait() + assert p.returncode == 0 From 17c8ecd332aa734ecba2bd048355a05089e568a5 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Sat, 20 Apr 2013 22:39:58 +0200 Subject: [PATCH 07/12] Run setup.py install before running tests Signed-off-by: Julien Danjou --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2b9beda..1136a62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,9 @@ python: - "3.3" - "2.6" # command to install dependencies -install: "pip install -r requirements.txt --use-mirrors" +install: + - pip install -r requirements.txt --use-mirrors + - python setup.py -q install # # command to run tests script: nosetests notifications: From 499ec7697ceb3d354073d05b85c46b5df8d01d98 Mon Sep 17 00:00:00 2001 From: John Jacobsen Date: Sun, 21 Apr 2013 09:03:19 -0500 Subject: [PATCH 08/12] Add context manger / 'with' statement to tutorial --- docs/tutorial.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 584652f..4f095c6 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -324,6 +324,18 @@ Comments start with semicolons: ; (print "but this will not") (+ 1 2 3) ; we'll execute the addition, but not this comment! +Python's context managers ('with' statements) are used like this: + +.. code-block:: clj + + (with [f (file "/tmp/data.in")] + (print (.read f))) + +which is equivalent to:: + + with file("/tmp/data.in") as f: + print f.read() + And yes, we do have lisp comprehensions! In Python you might do:: odds_squared = [ @@ -363,7 +375,7 @@ In hy, you could do these like: ; (8, 'A'), (8, 'B'), (8, 'C'), (8, 'D'), (8, 'E'), (8, 'F'), (8, 'G'), (8, 'H')] - + Protips! ======== @@ -409,6 +421,7 @@ TODO ==== - How do I define classes? + - How do I use context managers? - Blow your mind with macros! - Where's my banana??? - Mention that you can import .hy files in .py files and vice versa! From 44329227db357569ae58ffb33f6ac6dffe2be96b Mon Sep 17 00:00:00 2001 From: John Jacobsen Date: Sun, 21 Apr 2013 09:04:13 -0500 Subject: [PATCH 09/12] Remove unneeded line from TODO section --- docs/tutorial.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 4f095c6..748350a 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -421,7 +421,6 @@ TODO ==== - How do I define classes? - - How do I use context managers? - Blow your mind with macros! - Where's my banana??? - Mention that you can import .hy files in .py files and vice versa! From 38051ef96b3078fc22252f0067f27c587f7b2077 Mon Sep 17 00:00:00 2001 From: John Jacobsen Date: Sun, 21 Apr 2013 09:04:52 -0500 Subject: [PATCH 10/12] whitespace in doc --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 748350a..acb4481 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -375,7 +375,7 @@ In hy, you could do these like: ; (8, 'A'), (8, 'B'), (8, 'C'), (8, 'D'), (8, 'E'), (8, 'F'), (8, 'G'), (8, 'H')] - + Protips! ======== From 3f6bf5f27b5a1d3e3ebc5bcb0fa510d6024faeba Mon Sep 17 00:00:00 2001 From: John Jacobsen Date: Sun, 21 Apr 2013 09:27:11 -0500 Subject: [PATCH 11/12] more TODOs --- docs/tutorial.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index acb4481..1def911 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -420,6 +420,8 @@ Much more readable, no! Use the threading macro! TODO ==== + - How do I index into arrays or dictionaries? + - How do I do array ranges? e.g. x[5:] or y[2:10] - How do I define classes? - Blow your mind with macros! - Where's my banana??? From 592129502b57e1678b94336c59d96128711ebceb Mon Sep 17 00:00:00 2001 From: Paul Tagliamonte Date: Sun, 21 Apr 2013 10:53:34 -0400 Subject: [PATCH 12/12] Adding @egenhombre to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 9f04615..74d26c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -9,3 +9,4 @@ * Gergely Nagy * Konrad Hinsen * Vladimir Gorbunov +* John Jacobsen