Skip to content

Commit

Permalink
Merge pull request #2526 from Kodiologist/hy-IR
Browse files Browse the repository at this point in the history
Add `hy.R` and rename `hy.M` to `hy.I`
  • Loading branch information
Kodiologist committed Nov 10, 2023
2 parents dc3bed8 + 9645421 commit 43e5bcf
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 46 deletions.
3 changes: 3 additions & 0 deletions NEWS.rst
Expand Up @@ -20,6 +20,7 @@ Breaking Changes
* When a macro is `require`\d from another module, that module is no
longer implicitly included when checking for further macros in
the expansion.
* `hy.M` has been renamed to `hy.I`.
* `hy.eval` has been overhauled to be more like Python's `eval`. It
also has a new parameter `macros`.
* `hy.macroexpand` and `hy.macroexpand-1` have been overhauled and
Expand All @@ -38,6 +39,8 @@ New Features
* `defn`, `defn/a`, and `defclass` now support type parameters.
* `HyReader` now has an optional parameter to install existing
reader macros from the calling module.
* New syntax `(hy.R.aaa/bbb.m …)` for calling the macro `m` from the
module `aaa.bbb` without bringing `m` or `aaa.bbb` into scope.
* New pragma `warn-on-core-shadow`.
* `nonlocal` now also works for globally defined names.

Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Expand Up @@ -1369,7 +1369,7 @@ the following methods
.. hy:autofunction:: hy.as-model
.. hy:autoclass:: hy.M
.. hy:autoclass:: hy.I
.. _reader-macros:

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Expand Up @@ -73,7 +73,7 @@
)

import hy
hy.M = type(hy.M) # A trick to enable `hy:autoclass:: hy.M`
hy.I = type(hy.I) # A trick to enable `hy:autoclass:: hy.I`

# ** Generate Cheatsheet
import json
Expand Down
5 changes: 5 additions & 0 deletions docs/syntax.rst
Expand Up @@ -400,6 +400,11 @@ first element.
to construct a method call with the element after ``None`` as the object:
thus, ``(.add my-set 5)`` is equivalent to ``((. my-set add) 5)``, which
becomes ``my_set.add(5)`` in Python.

.. _hy.R:

- Exception: expressions like ``((. hy R module-name macro-name) …)``, or equivalently ``(hy.R.module-name.macro-name …)``, get special treatment. They import the module ``module-name`` and call its macro ``macro-name``, so ``(hy.R.foo.bar 1)`` is equivalent to ``(require foo) (foo.bar 1)``, but without bringing ``foo`` or ``foo.bar`` into scope. Thus ``hy.R`` is convenient syntactic sugar for macros you'll only call once in a file, or for macros that you want to appear in the expansion of other macros without having to call :hy:func:`require` in the expansion. As with :hy:class:`hy.I`, dots in the module name must be replaced with slashes.

- Otherwise, the expression is compiled into a Python-level call, with the
first element being the calling object. (So, you can call a function that has
the same name as a macro with an expression like ``((do setv) …)``.) The
Expand Down
15 changes: 6 additions & 9 deletions hy/__init__.py
Expand Up @@ -16,20 +16,17 @@ def _initialize_env_var(env_var, default_val):
# we import for side-effects.


class M:
"""``hy.M`` is an object that provides syntactic sugar for imports. It allows syntax like ``(hy.M.math.sqrt 2)`` to mean ``(import math) (math.sqrt 2)``, except without bringing ``math`` or ``math.sqrt`` into scope. This is useful in macros to avoid namespace pollution. To refer to a module with dots in its name, use slashes instead: ``hy.M.os/path.basename`` gets the function ``basename`` from the module ``os.path``.
class I:
"""``hy.I`` is an object that provides syntactic sugar for imports. It allows syntax like ``(hy.I.math.sqrt 2)`` to mean ``(import math) (math.sqrt 2)``, except without bringing ``math`` or ``math.sqrt`` into scope. (See :ref:`hy.R <hy.R>` for a version that requires a macro instead of importing a Python object.) This is useful in macros to avoid namespace pollution. To refer to a module with dots in its name, use slashes instead: ``hy.I.os/path.basename`` gets the function ``basename`` from the module ``os.path``.
You can also call ``hy.M`` like a function, as in ``(hy.M "math")``, which is useful when the module name isn't known until run-time. This interface just calls :py:func:`importlib.import_module`, avoiding (1) mangling due to attribute lookup, and (2) the translation of ``/`` to ``.`` in the module name. The advantage of ``(hy.M modname)`` over ``importlib.import_module(modname)`` is merely that it avoids bringing ``importlib`` itself into scope."""
You can also call ``hy.I`` like a function, as in ``(hy.I "math")``, which is useful when the module name isn't known until run-time. This interface just calls :py:func:`importlib.import_module`, avoiding (1) mangling due to attribute lookup, and (2) the translation of ``/`` to ``.`` in the module name. The advantage of ``(hy.I modname)`` over ``importlib.import_module(modname)`` is merely that it avoids bringing ``importlib`` itself into scope."""
def __call__(self, module_name):
import importlib
return importlib.import_module(module_name)
def __getattr__(self, s):
import re
return self(hy.mangle(re.sub(
r'/(-*)',
lambda m: '.' + '_' * len(m.group(1)),
hy.unmangle(s))))
M = M()
from hy.reader.mangling import slashes2dots
return self(slashes2dots(s))
I = I()


# Import some names on demand so that the dependent modules don't have
Expand Down
2 changes: 1 addition & 1 deletion hy/core/macros.hy
Expand Up @@ -136,7 +136,7 @@
(in name (getattr &compiler.module namespace {}))
`(get ~(hy.models.Symbol namespace) ~name)
(in name (getattr builtins namespace {}))
`(get (. hy.M.builtins ~(hy.models.Symbol namespace)) ~name)
`(get (. hy.I.builtins ~(hy.models.Symbol namespace)) ~name)
True
(raise (NameError (.format "no such {}macro: {!r}"
(if reader? "reader " "")
Expand Down
42 changes: 27 additions & 15 deletions hy/macros.py
Expand Up @@ -20,6 +20,7 @@
from hy.model_patterns import whole
from hy.models import Expression, Symbol, as_model, is_unpack, replace_hy_obj
from hy.reader import mangle
from hy.reader.mangling import slashes2dots

EXTRA_MACROS = ["hy.core.result_macros", "hy.core.macros"]

Expand Down Expand Up @@ -383,21 +384,32 @@ def macroexpand(tree, module, compiler=None, once=False, result_ok=True):
else:
break

# Choose the first namespace with the macro.
m = ((compiler and next(
(d[fn]
for d in [
compiler.extra_macros,
*(s['macros'] for s in reversed(compiler.local_state_stack))]
if fn in d),
None)) or
next(
(mod._hy_macros[fn]
for mod in (module, builtins)
if fn in getattr(mod, "_hy_macros", ())),
None))
if not m:
break
if fn.startswith('hy.R.'):
# Special syntax for a one-shot `require`.
req_from, _, fn = fn[len('hy.R.'):].partition('.')
req_from = slashes2dots(req_from)
try:
m = importlib.import_module(req_from)._hy_macros[fn]
except ImportError as e:
raise HyRequireError(e.args[0]).with_traceback(None)
except (AttributeError, KeyError):
raise HyRequireError(f'Could not require name {fn} from {req_from}')
else:
# Choose the first namespace with the macro.
m = ((compiler and next(
(d[fn]
for d in [
compiler.extra_macros,
*(s['macros'] for s in reversed(compiler.local_state_stack))]
if fn in d),
None)) or
next(
(mod._hy_macros[fn]
for mod in (module, builtins)
if fn in getattr(mod, "_hy_macros", ())),
None))
if not m:
break

with MacroExceptions(module, tree, compiler):
if compiler:
Expand Down
8 changes: 8 additions & 0 deletions hy/reader/mangling.py
Expand Up @@ -106,3 +106,11 @@ def unmangle(s):
s = s.replace("_", "-")

return prefix + s + suffix


def slashes2dots(s):
'Interpret forward slashes as a substitute for periods.'
return mangle(re.sub(
r'/(-*)',
lambda m: '.' + '_' * len(m.group(1)),
unmangle(s)))
2 changes: 1 addition & 1 deletion tests/native_tests/comprehensions.hy
Expand Up @@ -264,7 +264,7 @@


(defmacro eval-isolated [#* body]
`(hy.eval '(do ~@body) :module (hy.M.types.ModuleType "<test>") :locals {}))
`(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "<test>") :locals {}))


(defn test-lfor-nonlocal []
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/deftype.hy
Expand Up @@ -10,7 +10,7 @@
(assert (= Foo.__value__) int)

(deftype Foo (| int bool))
(assert (is (type Foo.__value__ hy.M.types.UnionType)))
(assert (is (type Foo.__value__ hy.I.types.UnionType)))

(deftype :tp [#^ int A #** B] Foo int)
(assert (= (ttp.show Foo) [
Expand Down
2 changes: 1 addition & 1 deletion tests/native_tests/hy_eval.hy
Expand Up @@ -248,7 +248,7 @@
(setv m (hy.read "(/ 1 0)" :filename "bad_math.hy"))
(with [e (pytest.raises ZeroDivisionError)]
(hy.eval m))
(assert (in "bad_math.hy" (get (hy.M.traceback.format-tb e.tb) -1))))
(assert (in "bad_math.hy" (get (hy.I.traceback.format-tb e.tb) -1))))


(defn test-eval-failure []
Expand Down
43 changes: 28 additions & 15 deletions tests/native_tests/hy_misc.hy
@@ -1,5 +1,5 @@
;; Tests of `hy.gensym`, `hy.macroexpand`, `hy.macroexpand-1`,
;; `hy.disassemble`, `hy.read`, and `hy.M`
;; `hy.disassemble`, `hy.read`, `hy.I`, and `hy.R`

(import
pytest)
Expand Down Expand Up @@ -29,7 +29,7 @@
(hy.macroexpand '(mac (a b) (mac 5)))
'(a b 5)))
(assert (=
(hy.macroexpand '(qplah "phooey") :module hy.M.tests.resources.tlib)
(hy.macroexpand '(qplah "phooey") :module hy.I.tests.resources.tlib)
'[8 "phooey"]))
(assert (=
(hy.macroexpand '(chippy 1) :macros
Expand Down Expand Up @@ -109,44 +109,44 @@
(assert (is (type (hy.read "0")) (type '0))))


(defn test-hyM []
(defn test-hyI []
(defmacro no-name [name]
`(with [(pytest.raises NameError)] ~name))

; `hy.M` doesn't bring the imported stuff into scope.
(assert (= (hy.M.math.sqrt 4) 2))
(assert (= (.sqrt (hy.M "math") 4) 2))
; `hy.I` doesn't bring the imported stuff into scope.
(assert (= (hy.I.math.sqrt 4) 2))
(assert (= (.sqrt (hy.I "math") 4) 2))
(no-name math)
(no-name sqrt)

; It's independent of bindings to such names.
(setv math (type "Dummy" #() {"sqrt" "hello"}))
(assert (= (hy.M.math.sqrt 4) 2))
(assert (= (hy.I.math.sqrt 4) 2))
(assert (= math.sqrt "hello"))

; It still works in a macro expansion.
(defmacro frac [a b]
`(hy.M.fractions.Fraction ~a ~b))
`(hy.I.fractions.Fraction ~a ~b))
(assert (= (* 6 (frac 1 3)) 2))
(no-name fractions)
(no-name Fraction)

; You can use `/` for dotted module names.
(assert (= (hy.M.os/path.basename "foo/bar") "bar"))
(assert (= (hy.I.os/path.basename "foo/bar") "bar"))
(no-name os)
(no-name path)

; `hy.M.__getattr__` attempts to cope with mangling.
; `hy.I.__getattr__` attempts to cope with mangling.
(with [e (pytest.raises ModuleNotFoundError)]
(hy.M.a-b☘c-d/e.z))
(hy.I.a-b☘c-d/e.z))
(assert (= e.value.name (hy.mangle "a-b☘c-d")))
; `hy.M.__call__` doesn't.
; `hy.I.__call__` doesn't.
(with [e (pytest.raises ModuleNotFoundError)]
(hy.M "a-b☘c-d/e.z"))
(hy.I "a-b☘c-d/e.z"))
(assert (= e.value.name "a-b☘c-d/e")))


(defn test-hyM-mangle-chain [tmp-path monkeypatch]
(defn test-hyI-mangle-chain [tmp-path monkeypatch]
; We can get an object from a submodule with various kinds of
; mangling in the name chain.

Expand All @@ -162,4 +162,17 @@
; don't reload it explicitly.
(import foo) (import importlib) (importlib.reload foo)

(assert (= hy.M.foo/foo?/_foo/☘foo☘/foo.foo 5)))
(assert (= hy.I.foo/foo?/_foo/☘foo☘/foo.foo 5)))


(defn test-hyR []
(assert (= (hy.R.tests/resources/tlib.qplah "x") [8 "x"]))
(assert (= (hy.R.tests/resources/tlib.✈ "x") "plane x"))
(with [(pytest.raises NameError)]
(hy.eval '(tests.resources.tlib.qplah "x")))
(with [(pytest.raises NameError)]
(hy.eval '(qplah "x")))
(with [(pytest.raises hy.errors.HyRequireError)]
(hy.eval '(hy.R.tests/resources/tlib.nonexistent-macro "x")))
(with [(pytest.raises hy.errors.HyRequireError)]
(hy.eval '(hy.R.nonexistent-module.qplah "x"))))
2 changes: 1 addition & 1 deletion tests/native_tests/let.hy
Expand Up @@ -520,7 +520,7 @@


(defmacro eval-isolated [#* body]
`(hy.eval '(do ~@body) :module (hy.M.types.ModuleType "<test>") :locals {}))
`(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "<test>") :locals {}))


(defn test-let-bound-nonlocal []
Expand Down

0 comments on commit 43e5bcf

Please sign in to comment.