Repository: hylang/hy

Issue:
Official support for evaluating strings of Hy code from Python
Is it possible to embed some hy code inside a python file? As opposed to having the whole file be full on hy?

Discussion:
Kodiologist: Generally, this looks good.

- See `CONTRIBUTING.rst` for guidelines about commit messages, `NEWS`, and `AUTHORS`.
- In `test_importer`, import `read_str` and `hy_eval` directly from `hy`, since that's the interface you want to test.
- Define `eval_str` inside `test_eval`, since it's only used there.

Kodiologist: Also, could you describe the `eof` parameter of `read` in its docstring? Alternatively, `read` could perhaps be rewritten without it. The current implementation looks kind of weird.
jdanford: All fixes have been made!
Kodiologist: Cool. Don't forget about https://github.com/hylang/hy/pull/1369#issuecomment-320468126.

Patch:
diff --git a/AUTHORS b/AUTHORS
index dcc9b0ce..7791140d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -80,3 +80,4 @@
 * Neil Lindquist <archer1mail@gmail.com
 * Hikaru Ikuta <woodrush924@gmail.com>
 * David Schaefer <david.schaefe@gmail.com>
+* Jordan Danford <jordandanford@gmail.com>
diff --git a/NEWS b/NEWS
index 6580965e..4a2a9e59 100644
--- a/NEWS
+++ b/NEWS
@@ -27,6 +27,10 @@ Changes from 0.13.0
    * `exec` now works under Python 2
    * No TypeError from multi-arity defn returning values evaluating to None
 
+   [ Misc. Improvements ]
+   * `read`, `read_str`, and `eval` are exposed and documented as top-level
+     functions in the `hy` module
+
 Changes from 0.12.1
 
    [ Language Changes ]
diff --git a/docs/language/interop.rst b/docs/language/interop.rst
index 433679cc..039d825c 100644
--- a/docs/language/interop.rst
+++ b/docs/language/interop.rst
@@ -131,5 +131,17 @@ argument::
     test()
     bar
 
+Evaluating strings of Hy code from Python
+-----------------------------------------
 
+Evaluating a string (or ``file`` object) containing a Hy expression requires
+two separate steps. First, use the ``read_str`` function (or ``read`` for a
+``file`` object) to turn the expression into a Hy model::
 
+    >>> import hy
+    >>> expr = hy.read_str("(- (/ (+ 1 3 88) 2) 8)")
+
+Then, use the ``eval`` function to evaluate it::
+
+    >>> hy.eval(expr)
+    38.0
diff --git a/hy/__init__.py b/hy/__init__.py
index a17ddbbe..6b986d19 100644
--- a/hy/__init__.py
+++ b/hy/__init__.py
@@ -10,3 +10,7 @@ from hy.models import HyExpression, HyInteger, HyKeyword, HyComplex, HyString, H
 
 import hy.importer  # NOQA
 # we import for side-effects.
+
+
+from hy.core.language import read, read_str  # NOQA
+from hy.importer import hy_eval as eval  # NOQA
diff --git a/hy/core/language.hy b/hy/core/language.hy
index 5f9378ad..4504b6ab 100644
--- a/hy/core/language.hy
+++ b/hy/core/language.hy
@@ -426,8 +426,8 @@
 
 (defn read [&optional [from-file sys.stdin]
                       [eof ""]]
-  "Read from input and returns a tokenized string.
-   Can take a given input buffer to read from"
+  "Read from input and returns a tokenized string. Can take a given input buffer
+   to read from, and a single byte as EOF (defaults to an empty string)"
   (setv buff "")
   (while True
     (setv inn (string (.readline from-file)))


Test patch:
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
index 79b72daa..9fa5aa59 100644
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -2,12 +2,14 @@
 # This file is part of Hy, which is free software licensed under the Expat
 # license. See the LICENSE.
 
+import hy
 from hy.importer import (import_file_to_module, import_buffer_to_ast,
                          MetaLoader, get_bytecode_path)
 from hy.errors import HyTypeError
 import os
 import ast
 import tempfile
+from fractions import Fraction
 
 
 def test_basics():
@@ -64,3 +66,20 @@ def test_import_autocompiles():
 
     os.remove(f.name)
     os.remove(get_bytecode_path(f.name))
+
+
+def test_eval():
+    def eval_str(s):
+        return hy.eval(hy.read_str(s))
+
+    assert eval_str('[1 2 3]') == [1, 2, 3]
+    assert eval_str('{"dog" "bark" "cat" "meow"}') == {
+        'dog': 'bark', 'cat': 'meow'}
+    assert eval_str('(, 1 2 3)') == (1, 2, 3)
+    assert eval_str('#{3 1 2}') == {1, 2, 3}
+    assert eval_str('1/2') == Fraction(1, 2)
+    assert eval_str('(.strip " fooooo   ")') == 'fooooo'
+    assert eval_str(
+        '(if True "this is if true" "this is if false")') == "this is if true"
+    assert eval_str('(list-comp (pow num 2) (num (range 100)) (= (% num 2) 1))') == [
+        pow(num, 2) for num in range(100) if num % 2 == 1]