From e94f6c7a22192f9b434483c39351017f860890b4 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 6 Nov 2023 14:07:40 -0500 Subject: [PATCH 1/4] =?UTF-8?q?Mass=20replacement:=20`hy.M`=20=E2=86=92=20?= =?UTF-8?q?`hy.I`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api.rst | 2 +- docs/conf.py | 2 +- hy/__init__.py | 8 ++++---- hy/core/macros.hy | 2 +- tests/native_tests/comprehensions.hy | 2 +- tests/native_tests/deftype.hy | 2 +- tests/native_tests/hy_eval.hy | 2 +- tests/native_tests/hy_misc.hy | 30 ++++++++++++++-------------- tests/native_tests/let.hy | 2 +- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index b37ef5aeb..69344d4d0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1369,7 +1369,7 @@ the following methods .. hy:autofunction:: hy.as-model -.. hy:autoclass:: hy.M +.. hy:autoclass:: hy.I .. _reader-macros: diff --git a/docs/conf.py b/docs/conf.py index 3d6be0873..e12278d77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 diff --git a/hy/__init__.py b/hy/__init__.py index 03fc2e2d5..3a38caec5 100644 --- a/hy/__init__.py +++ b/hy/__init__.py @@ -16,10 +16,10 @@ 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. 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) @@ -29,7 +29,7 @@ def __getattr__(self, s): r'/(-*)', lambda m: '.' + '_' * len(m.group(1)), hy.unmangle(s)))) -M = M() +I = I() # Import some names on demand so that the dependent modules don't have diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 878221f83..4eab02e17 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -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 " "") diff --git a/tests/native_tests/comprehensions.hy b/tests/native_tests/comprehensions.hy index bc7166759..bc354a4cb 100644 --- a/tests/native_tests/comprehensions.hy +++ b/tests/native_tests/comprehensions.hy @@ -264,7 +264,7 @@ (defmacro eval-isolated [#* body] - `(hy.eval '(do ~@body) :module (hy.M.types.ModuleType "") :locals {})) + `(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "") :locals {})) (defn test-lfor-nonlocal [] diff --git a/tests/native_tests/deftype.hy b/tests/native_tests/deftype.hy index 8be8a9e3b..3fb6e0e25 100644 --- a/tests/native_tests/deftype.hy +++ b/tests/native_tests/deftype.hy @@ -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) [ diff --git a/tests/native_tests/hy_eval.hy b/tests/native_tests/hy_eval.hy index 479b5907a..3abf52f35 100644 --- a/tests/native_tests/hy_eval.hy +++ b/tests/native_tests/hy_eval.hy @@ -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 [] diff --git a/tests/native_tests/hy_misc.hy b/tests/native_tests/hy_misc.hy index 07a03dff4..37992d42f 100644 --- a/tests/native_tests/hy_misc.hy +++ b/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`, and `hy.I` (import pytest) @@ -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 @@ -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. @@ -162,4 +162,4 @@ ; 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))) diff --git a/tests/native_tests/let.hy b/tests/native_tests/let.hy index db7890b72..94f0b1122 100644 --- a/tests/native_tests/let.hy +++ b/tests/native_tests/let.hy @@ -520,7 +520,7 @@ (defmacro eval-isolated [#* body] - `(hy.eval '(do ~@body) :module (hy.M.types.ModuleType "") :locals {})) + `(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "") :locals {})) (defn test-let-bound-nonlocal [] From d67299d3ca9c95a05e9311c64c6e762f8c5dd91e Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 6 Nov 2023 14:11:52 -0500 Subject: [PATCH 2/4] Update NEWS --- NEWS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.rst b/NEWS.rst index 8f111d4b1..2ecf76765 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -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 From f4729a28cea76a617e5bd95982aafd9922f8744d Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 6 Nov 2023 15:19:10 -0500 Subject: [PATCH 3/4] Spin out part of `hy.I` into `slashes2dots` --- hy/__init__.py | 7 ++----- hy/reader/mangling.py | 8 ++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/hy/__init__.py b/hy/__init__.py index 3a38caec5..dcd95c94c 100644 --- a/hy/__init__.py +++ b/hy/__init__.py @@ -24,11 +24,8 @@ 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)))) + from hy.reader.mangling import slashes2dots + return self(slashes2dots(s)) I = I() diff --git a/hy/reader/mangling.py b/hy/reader/mangling.py index 74b2c932b..538c38c4f 100644 --- a/hy/reader/mangling.py +++ b/hy/reader/mangling.py @@ -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))) From 9645421fe87b9772c9cfc1268fe290752eb97077 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Wed, 8 Nov 2023 09:06:12 -0500 Subject: [PATCH 4/4] Add `hy.R` --- NEWS.rst | 2 ++ docs/syntax.rst | 5 +++++ hy/__init__.py | 2 +- hy/macros.py | 42 ++++++++++++++++++++++------------- tests/native_tests/hy_misc.hy | 15 ++++++++++++- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 2ecf76765..f35350399 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -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. diff --git a/docs/syntax.rst b/docs/syntax.rst index 54188fda5..ed4a347aa 100644 --- a/docs/syntax.rst +++ b/docs/syntax.rst @@ -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 diff --git a/hy/__init__.py b/hy/__init__.py index dcd95c94c..afa43dcd4 100644 --- a/hy/__init__.py +++ b/hy/__init__.py @@ -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 ` 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): diff --git a/hy/macros.py b/hy/macros.py index 90dae35b6..0c028ccc1 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -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"] @@ -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: diff --git a/tests/native_tests/hy_misc.hy b/tests/native_tests/hy_misc.hy index 37992d42f..8109aa065 100644 --- a/tests/native_tests/hy_misc.hy +++ b/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.I` +;; `hy.disassemble`, `hy.read`, `hy.I`, and `hy.R` (import pytest) @@ -163,3 +163,16 @@ (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 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"))))