Skip to content

Commit

Permalink
Add hy.R
Browse files Browse the repository at this point in the history
  • Loading branch information
Kodiologist committed Nov 6, 2023
1 parent 46fca11 commit 89218cb
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 17 deletions.
2 changes: 2 additions & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,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
5 changes: 5 additions & 0 deletions docs/syntax.rst
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion hy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _initialize_env_var(env_var, default_val):


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. 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``.
"""``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.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):
Expand Down
42 changes: 27 additions & 15 deletions hy/macros.py
Original file line number Diff line number Diff line change
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
11 changes: 10 additions & 1 deletion tests/native_tests/hy_misc.hy
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
;; Tests of `hy.gensym`, `hy.macroexpand`, `hy.macroexpand-1`,
;; `hy.disassemble`, `hy.read`, and `hy.I`
;; `hy.disassemble`, `hy.read`, `hy.I`, and `hy.R`

(import
pytest)
Expand Down Expand Up @@ -163,3 +163,12 @@
(import foo) (import importlib) (importlib.reload foo)

(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 hy.errors.HyRequireError)]
(hy.eval '(hy.R.tests/resources/tlib.flarhgunnstow "x")))
(with [(pytest.raises hy.errors.HyRequireError)]
(hy.eval '(hy.R.nonexistent-module.qplah "x"))))

0 comments on commit 89218cb

Please sign in to comment.