Skip to content

Commit

Permalink
Merge pull request hylang#2425 from Kodiologist/import-sugar
Browse files Browse the repository at this point in the history
Add `hy.M` import sugar
  • Loading branch information
Kodiologist authored Apr 6, 2023
2 parents 8ee37b0 + 2585c33 commit 1b4efba
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 3 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ jobs:
shell: bash
run: |
if [[ ${{ matrix.python }} = pyodide ]] ; then
npm install pyodide
npm install [email protected]
# 0.23.0 has a regression: https://github.com/pyodide/pyodide/issues/3730
pip install 'pip >= 22.3.1'
# Older pips may fail to install `pyodide-build`.
pip install pyodide-build
pip install 'pyodide-build == 0.22.1'
pyodide venv .venv-pyodide
source .venv-pyodide/bin/activate
fi
Expand Down
1 change: 1 addition & 0 deletions NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ New Features
case they're no-ops.
* The `py` macro now implicitly parenthesizes the input code, so Python's
indentation restrictions don't apply.
* New built-in object `hy.M` for easy imports in macros.

0.26.0 (released 2023-02-08)
=============================
Expand Down
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,8 @@ the following methods
.. hy:autofunction:: hy.as-model
.. hy:autoclass:: hy.M
.. _reader-macros:

Reader Macros
Expand Down
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
py3_10=("https://docs.python.org/3.10/", None),
hyrule=("https://hyrule.readthedocs.io/en/master/", None),
)

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

# ** Generate Cheatsheet
import json
from itertools import zip_longest
Expand Down
17 changes: 17 additions & 0 deletions hy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ def _initialize_env_var(env_var, default_val):
hy.importer._inject_builtins()
# 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``.
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."""
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()


# Import some names on demand so that the dependent modules don't have
# to be loaded if they're not needed.

Expand Down
58 changes: 57 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`, and `hy.read`
;; `hy.disassemble`, `hy.read`, and `hy.M`

(import
pytest)
Expand Down Expand Up @@ -86,3 +86,59 @@
(assert (is (type (hy.read "[]")) (type '[])))
(assert (= (hy.read "0") '0))
(assert (is (type (hy.read "0")) (type '0))))


(defn test-hyM []
(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))
(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 (= math.sqrt "hello"))

; It still works in a macro expansion.
(defmacro frac [a b]
`(hy.M.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"))
(no-name os)
(no-name path)

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


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

(setv p tmp-path)
(for [e ["foo" "foo?" "_foo" "☘foo☘"]]
(/= p (hy.mangle e))
(.mkdir p :exist-ok True)
(.write-text (/ p "__init__.py") ""))
(.write-text (/ p "foo.hy") "(setv foo 5)")
(monkeypatch.syspath-prepend (str tmp-path))

; Python will reuse any `foo` imported in an earlier test if we
; don't reload it explicitly.
(import foo) (import importlib) (importlib.reload foo)

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

0 comments on commit 1b4efba

Please sign in to comment.