diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml deleted file mode 100644 index 959f9f95a..000000000 --- a/.github/workflows/static-analysis.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Perform static analysis on Python files - -on: - pull_request: {} - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 30d8c0de9..f1b2a7008 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,13 +10,16 @@ jobs: matrix: name-prefix: [''] os: [ubuntu-latest] - python: [3.7, 3.8, 3.9, '3.10', pypy-3.8] + python: [3.8, 3.9, '3.10', 3.11, 3.12, pypy-3.10, pyodide] include: - # To keep the overall number of runs low, we test Windows + # To keep the overall number of runs low, we test Windows and MacOS # only on the latest CPython. - name-prefix: 'win-' os: windows-latest - python: '3.10' + python: 3.11 + - name-prefix: 'mac-' + os: macos-latest + python: 3.11 name: ${{ format('{0}{1}', matrix.name-prefix, matrix.python) }} runs-on: ${{ matrix.os }} @@ -28,12 +31,37 @@ jobs: steps: - run: git config --global core.autocrlf false - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v3 + - if: ${{ matrix.python != 'pyodide' }} + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - - run: pip install . && rm -r hy - # We want to be sure we're testing the installed version, - # instead of running from the source tree. - - run: pip install pytest - - run: pytest + - if: ${{ matrix.python == 'pyodide' }} + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - if: ${{ matrix.python == 'pyodide' }} + uses: actions/setup-node@v3 + - name: Install + shell: bash + run: | + if [[ ${{ matrix.python }} = pyodide ]] ; then + pip install 'pydantic < 2' + # https://github.com/pyodide/pyodide/pull/3971 + npm install pyodide + pip install pyodide-build + pyodide venv .venv-pyodide + source .venv-pyodide/bin/activate + fi + pip install . + rm -r hy + # We want to be sure we're testing the installed version, + # instead of running from the source tree. + pip install pytest + - name: Test + shell: bash + run: | + if [[ ${{ matrix.python }} = pyodide ]] ; then + source .venv-pyodide/bin/activate + fi + python -m pytest tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 6e69632c8..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,16 +0,0 @@ -exclude: "^docs/" -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..ede665a04 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + builder: html + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + - requirements: requirements-dev.txt diff --git a/AUTHORS b/AUTHORS index 194456f09..9702eadb7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -33,7 +33,7 @@ * Matt Fenwick * Sean B. Palmer * Thom Neale -* Tuukka Turto +* Tuula Turto * Vasudev Kamath * Yuval Langer * Fatih Kadir Akın @@ -109,3 +109,4 @@ * Dmitry Ivanov * Andrey Vlasovskikh * Joseph LaFreniere +* Daniel Tan diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index b26b6d5ec..d3658dd06 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,7 +8,9 @@ helps in making Hy better. Potential contributions include: - Requesting features. - Adding features. - Writing tests for outstanding bugs or untested features. + - You can mark tests that Hy can't pass yet as xfail_. + - Cleaning up the code. - Improving the documentation. - Answering questions on `the Github Discussions page`_ or @@ -67,18 +69,10 @@ The first line of a commit message should describe the overall change in 50 characters or less. If you wish to add more information, separate it from the first line with a blank line. -Code formatting ---------------- - -All Python source code (``.py``) should be formatted with ``black`` and ``isort``. -This can be accomplished by running ``black hy tests`` and ``isort hy tests`` from the root of this repository. -Formatting of Python files is checked automatically via GitHub Actions for all pull requests. -No PR may be merged if it fails that check. - Testing ------- -Tests can be run by executing `pytest` in the root of this repository. +Tests can be run by executing ``pytest`` in the root of this repository. New features and bug fixes should be tested. If you've caused an xfail_ test to start passing, remove the xfail mark. If you're @@ -89,6 +83,11 @@ No PR may be merged if it causes any tests to fail. The byte-compiled versions of the test files can be purged using ``git clean -dfx tests/``. If you want to run the tests while skipping the slow ones in ``test_bin.py``, use ``pytest --ignore=tests/test_bin.py``. +Documentation +------------- + +Generally, new features deserve coverage in the manual, either by editing the manual files directly or by changing docstrings that get included in the manual. To render the manual, install its dependencies with ``pip install -r requirements-dev.txt`` and then use the command ``cd docs; sphinx-build . _build -b html``. + NEWS and AUTHORS ---------------- @@ -120,9 +119,7 @@ There are two situations in which a PR is allowed to be merged: author. Changes to the documentation, or trivial changes to code, need only **one** approving member. 2. When the PR is at least **three days** old and **no** member of the Hy core - team has expressed disapproval of the PR in its current state. (Exception: a - PR to create a new release is not eligible to be merged under this criterion, - only the first one.) + team has expressed disapproval of the PR in its current state. Anybody on the Hy core team may perform the merge. Merging should create a merge commit (don't squash unnecessarily, because that would remove separation between diff --git a/LICENSE b/LICENSE index a0d3b8150..1247a0ad2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2022 the authors. +Copyright 2023 the authors. Portions of setup.py, copyright 2016 Jason R Coombs . Permission is hereby granted, free of charge, to any person obtaining a diff --git a/NEWS.rst b/NEWS.rst index 0df14fc63..72f2b470a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,21 +1,193 @@ .. default-role:: code Unreleased +============================= + +Removals +------------------------------ +* `delmacro` has been removed. Use `(del (get _hy_macros (hy.mangle + …)))` instead. +* `hy.reserved` has been removed. Use `(.keys (builtins._hy_macros))` + or Python's built-in `keyword` module instead. +* `doc` has been removed. Use `(help (get-macro foo))` or `(help + (get-macro :reader foo))` instead. +* The environment variables `HY_DEBUG` and `HY_FILTER_INTERNAL_ERRORS` + have been replaced with `HY_SHOW_INTERNAL_ERRORS`. + +Breaking Changes +------------------------------ + +* `defmacro` and `require` can now define macros locally instead of + only module-wide. +* 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 + generalized to include more of the features of `hy.eval`. +* `hy` now only implicitly launches a REPL if standard input is a TTY. +* `hy -i` has been overhauled to work as a flag like `python3 -i`. +* `hy2py` now requires `-m` to specify modules, and uses + the same `sys.path` rules as Python when parsing a module + vs a standalone script. +* New macro `deftype`. +* New macro `get-macro`. +* New macro `local-macros`. + +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. + +Misc. Improvements +------------------------------ +* Some syntax errors raised by core macros now have more informative + messages. +* Logical operators now compile to simpler Python code in some cases. + +Bug Fixes +------------------------------ +* Double quotes inside of bracketed f-strings are now properly handled. +* Fixed incomplete recognition of macro calls with a unary dotted + head like `((. defn) f [])`. +* `~@ #*` now produces a syntax error instead of a nonsensical result. +* Fixed parsing of infinite and NaN imaginary literals with an + uppercase "J". +* Fixed `hy.eval` failing on `defreader` or `require` forms that + install a new reader. +* `require` now warns when you shadow a core macro, like `defmacro` + already did. +* `nonlocal` now works for top-level `let`-bound names. +* `hy -i` with a filename now skips shebang lines. +* Implicit returns are now disabled in async generators. +* The parameter `result-ok` that was mistakenly included in the + signature of `hy.macroexpand` is now gone. + +0.27.0 (released 2023-07-06) +============================= + +Removals +------------------------------ +* Python 3.7 is no longer supported. + +Breaking Changes +------------------------------ +* Reader macros now always read a full identifier after the initial + `#`. Thus, `#*foo` is now parsed as a call to the reader macro named + `*foo`; to unpack a variable named `foo`, say `#* foo`. +* The names of reader macros names are no longer mangled. +* Question marks (`?`) are no longer mangled specially, so `foo?` now + mangles to `hyx_fooXquestion_markX` instead of `is_foo`. +* `hy2py`'s recursive mode now expects a module name as input, not any + old directory. You must be in the parent directory of the module + directory. + +New Features +------------------------------ +* Python 3.12 is now supported. +* New built-in object `hy.M` for easy imports in macros. +* `cut` now has a function version in `hy.pyops`. +* The `py` macro now implicitly parenthesizes the input code, so + Python's indentation restrictions don't apply. +* `try` no longer requires `except`, `except*`, or `finally`, and it + allows `else` even without `except` or `except*`. +* `nonlocal` and `global` can now be called with no arguments, in + which case they're no-ops. +* For easier reading, `hy --spy` now prints a delimiter after the + Python equivalent of your code, before the result of evaluating the + code. + +Bug Fixes +------------------------------ +* Fixed an installation failure in some situations when version lookup + fails. +* Fixed some bugs with traceback pointing. +* Fixed some bugs with escaping in bracket f-strings +* The parser no longer looks for shebangs in the REPL or `hy -c`. +* `require` with relative module names should now work correctly with + `hy -m`, as well as `hy2py`'s recursive mode. +* `hy.models.Symbol` no longer allows constructing a symbol beginning + with `#`. + +0.26.0 (released 2023-02-08) +============================= + +Removals +------------------------------ +* Coloring error messages and Python representations for models is no + longer supported. (Thus, Hy no longer depends on `colorama`.) + +Breaking Changes +------------------------------ +* Various warts have been smoothed over in the syntax of `'`, + \`, `~`, and `~@`: + + * Whitespace is now allowed after these syntactic elements. Thus one + can apply `~` to a symbol whose name begins with "@". + * \` and `~` are no longer allowed in identifiers. (This was already + the case for `'`.) + * The bitwise NOT operator `~` has been renamed to `bnot`. + +* Dotted identifiers like `foo.bar` and `.sqrt` now parse as + expressions (like `(. foo bar)` and `(. None sqrt)`) instead of + symbols. Some odd cases like `foo.` and `foo..bar` are now + syntactically illegal. +* New macro `do-mac`. +* New macro `pragma` (although it doesn't do anything useful yet). +* `hy.cmdline.HyREPL` is now `hy.REPL`. +* Redundant scripts named `hy3`, `hyc3`, and `hy2py3` are no longer + installed. Use `hy`, `hyc`, and `hy2py` instead. + +New Features +------------------------------ +* Pyodide is now officially supported. +* `.`, `..`, etc. are now usable as ordinary symbols (with the + remaining special rule that `...` compiles to `Ellipsis`). +* On Pythons ≥ 3.7, Hy modules can now be imported from ZIP + archives in the same way as Python modules, via `zipimport`_. +* `hy2py` has a new command-line option `--output`. +* `hy2py` can now operate recursively on a directory. + +Bug Fixes +------------------------------ +* `hy.REPL` now restores the global values it changes (such as + `sys.ps1`) after `hy.REPL.run` terminates. +* `hy.REPL` no longer mixes up Hy's and Python's Readline histories + when run inside Python's REPL. +* Fixed `hy.repr` of non-compilable uses of sugared macros, such as + `(quote)` and `(quote 1 2)`. + +.. _zipimport: https://docs.python.org/3.11/library/zipimport.html + +0.25.0 (released 2022-11-08) ============================== -Other Breaking Changes +Breaking Changes ------------------------------ * `dfor` no longer requires brackets around its final arguments, so `(dfor x (range 5) [x (* 2 x)])` is now `(dfor x (range 5) x (* 2 x))`. +* `except*` (PEP 654) is now recognized in `try`, and a placeholder + macro for `except*` has been added. Bug Fixes ------------------------------ -* Fixed `hy.repr` of `slice` objects with non-integer arguments. * `__file__` should now be set the same way as in Python. +* `\N{…}` escape sequences are now recognized in f-strings. * Fixed a bug with `python -O` where assertions were still partly evaluated. -* `\N{…}` escape sequences are now recognized in f-strings. +* Fixed `hy.repr` of `slice` objects with non-integer arguments. + +New Features +------------------------------ +* Python 3.11 is now supported. Misc. Improvements ------------------------------ @@ -78,24 +250,6 @@ Other Breaking Changes * `hy.cmdline.run_repl` has been replaced with `hy.cmdline.HyREPL.run`. -Bug Fixes ------------------------------- -* Fixed a crash when using keyword objects in `match`. -* Fixed a scoping bug in comprehensions in `let` bodies. -* Literal newlines (of all three styles) are now recognized properly - in string and bytes literals. -* `defmacro` no longer allows further arguments after `#* args`. -* `!=` with model objects is now consistent with `=`. -* Tracebacks from code parsed with `hy.read` now show source - positions. -* Elements of `builtins` such as `help` are no longer overridden until - the REPL actually starts. -* Readline is now imported only when necessary, to avoid triggering a - CPython bug regarding the standard module `curses` - (`cpython#46927`_). -* Module names supplied to `hy -m` are now mangled. -* Hy now precompiles its own Hy code during installation. - New Features ------------------------------ * Added user-defined reader macros, defined with `defreader`. @@ -118,6 +272,24 @@ New Features * Added a command-line option `-u` (or `--unbuffered`) per CPython. * Tab-completion in the REPL now attempts to unmangle names. +Bug Fixes +------------------------------ +* Fixed a crash when using keyword objects in `match`. +* Fixed a scoping bug in comprehensions in `let` bodies. +* Literal newlines (of all three styles) are now recognized properly + in string and bytes literals. +* `defmacro` no longer allows further arguments after `#* args`. +* `!=` with model objects is now consistent with `=`. +* Tracebacks from code parsed with `hy.read` now show source + positions. +* Elements of `builtins` such as `help` are no longer overridden until + the REPL actually starts. +* Readline is now imported only when necessary, to avoid triggering a + CPython bug regarding the standard module `curses` + (`cpython#46927`_). +* Module names supplied to `hy -m` are now mangled. +* Hy now precompiles its own Hy code during installation. + .. _cpython#46927: https://github.com/python/cpython/issues/46927#issuecomment-1093418916 .. _cpython#90678: https://github.com/python/cpython/issues/90678 @@ -156,6 +328,14 @@ Other Breaking Changes would be syntactically legal as a literal. * `hy.extra.reserved` has been renamed to `hy.reserved`. +New Features +------------------------------ +* `hy.repr` now supports several more standard types. +* The attribute access macro `.` now allows method calls. For example, + `(. x (f a))` is equivalent to `(x.f a)`. +* `hy.as-model` checks for self-references in its argument. +* New function `hy.model_patterns.keepsym`. + Bug Fixes ------------------------------ * In comprehension forms other than `for`, assignments (other than @@ -172,14 +352,6 @@ Bug Fixes * Fixed a bug with self-requiring files on Windows. * Improved error messages for illegal uses of `finally` and `else`. -New Features ------------------------------- -* `hy.repr` now supports several more standard types. -* The attribute access macro `.` now allows method calls. For example, - `(. x (f a))` is equivalent to `(x.f a)`. -* `hy.as-model` checks for self-references in its argument. -* New function `hy.model_patterns.keepsym`. - .. _Hyrule: https://github.com/hylang/hyrule 1.0a3 (released 2021-07-09) diff --git a/README.md b/README.md index 8cc4d9270..9d1e32221 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ To install the latest release of Hy, just use the command `pip3 install --user hy`. Then you can start an interactive read-eval-print loop (REPL) with the command `hy`, or run a Hy program with `hy myprogram.hy`. -* [Try Hy with a web console](https://hylang.github.io/hy-interpreter) +Hy is tested on all released and currently maintained versions of CPython (on +Linux and Windows), and on recent versions of PyPy and Pyodide. + +* [The Hy homepage](http://hylang.org) +* [Try Hy with a web console](http://hylang.org/try-hy) * [Why Hy?](http://docs.hylang.org/en/stable/whyhy.html) * [Tutorial](http://docs.hylang.org/en/stable/tutorial.html) @@ -25,7 +29,7 @@ Project * Code: https://github.com/hylang/hy * Documentation: * master, for use with the latest revision on GitHub: http://docs.hylang.org/en/master - * stable, for use with the latest release on PyPI: http://hylang.org/en/stable + * stable, for use with the latest release on PyPI: http://docs.hylang.org/en/stable * Bug reports: We have no bugs! Your bugs are your own! (https://github.com/hylang/hy/issues) * License: MIT (Expat) * [Hacking on Hy](http://docs.hylang.org/en/master/hacking.html) @@ -34,6 +38,8 @@ Project * Community: Join us on [Github Discussions](https://github.com/hylang/hy/discussions)! * [Stack Overflow: The [hy] tag](https://stackoverflow.com/questions/tagged/hy) +Hy's current maintainer is [Kodi Arfer](https://github.com/Kodiologist). He takes responsibility for answering user questions, which should primarily be asked on Stack Overflow or GitHub Discussions, but feel free to [poke him](http://arfer.net/elsewhere) if he's missed a question or you've found a serious security issue. + ![Cuddles the Hacker](https://i.imgur.com/QbPMXTN.png) (fan art from the one and only [doctormo](http://doctormo.deviantart.com/art/Cuddles-the-Hacker-372184766)) diff --git a/conftest.py b/conftest.py index 6b28427d3..815d86d89 100644 --- a/conftest.py +++ b/conftest.py @@ -1,41 +1,18 @@ -import importlib import os -import sys -from functools import reduce -from operator import or_ from pathlib import Path -import pytest +import hy, pytest -import hy -from hy._compat import PY3_8, PY3_10 - -NATIVE_TESTS = os.path.join("", "tests", "native_tests", "") +NATIVE_TESTS = Path.cwd() / "tests/native_tests" # https://github.com/hylang/hy/issues/2029 os.environ.pop("HYSTARTUP", None) -def pytest_ignore_collect(path, config): - versions = [ - (sys.version_info < (3, 8), "sub_py3_7_only"), - (PY3_8, "py3_8_only"), - (PY3_10, "py3_10_only"), - ] - - return ( - reduce( - or_, - (name in path.basename and not condition for condition, name in versions), - ) - or None - ) - - -def pytest_collect_file(parent, path): +def pytest_collect_file(file_path, parent): if ( - path.ext == ".hy" - and NATIVE_TESTS in path.dirname + os.sep - and path.basename != "__init__.hy" + file_path.suffix == ".hy" + and NATIVE_TESTS in file_path.parents + and file_path.name != "__init__.hy" ): - return pytest.Module.from_parent(parent, path=Path(path)) + return pytest.Module.from_parent(parent, path=file_path) diff --git a/docs/api.rst b/docs/api.rst index ecfabd77f..c67c69432 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,115 +1,105 @@ API === +.. _core-macros: Core Macros ----------- -The following macros are auto imported into all Hy modules as their +The following macros are automatically imported into all Hy modules as their base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. -.. hy:data:: #^ +.. hy:macro:: (annotate [value type]) - The ``#^`` symbol is used to denote annotations in three different contexts: + ``annotate`` and its shorthand form ``#^`` are used to denote annotations, + including type hints, in three different contexts: - - Standalone variable annotations. - - Variable annotations in a setv call. - - Function argument annotations. + - Standalone variable annotations (:pep:`526`) + - Variable annotations in a :hy:func:`setv` call + - Function-parameter annotations (:pep:`3107`) - They implement `PEP 526 `_ and - `PEP 3107 `_. + The difference between ``annotate`` and ``#^`` is that ``annotate`` requires + parentheses and takes the name to be annotated first (like Python), whereas + ``#^`` doesn't require parentheses (it only applies to the next two forms) + and takes the type second:: - Syntax sugar for :hy:func:`annotate` where the type comes first. - - Here is some example syntax of all three usages: + (setv (annotate x int) 1) + (setv #^ int x 1) - :strong:`Examples` + The order difference is not merely visual: ``#^`` actually evaluates the + type first. - :: + Here are examples with ``#^`` for all the places you can use annotations:: - ; Annotate the variable x as an int (equivalent to `x: int`). - #^int x - ; Can annotate with expressions if needed (equivalent to `y: f(x)`). + ; Annotate the variable `x` as an `int` (equivalent to `x: int`). + #^ int x + ; You can annotate with expressions (equivalent to `y: f(x)`). #^(f x) y - ; Annotations with an assignment: each annotation (int, str) covers the term that - ; immediately follows. - ; Equivalent to: x: int = 1; y = 2; z: str = 3 - (setv #^int x 1 y 2 #^str z 3) + ; Annotations with an assignment: each annotation `(int, str)` + ; covers the term that immediately follows. + ; Equivalent to `x: int = 1; y = 2; z: str = 3` + (setv #^ int x 1 y 2 #^ str z 3) - ; Annotate a as an int, c as an int, and b as a str. - ; Equivalent to: def func(a: int, b: str = None, c: int = 1): ... - (defn func [#^int a #^str [b None] #^int [c 1]] ...) + ; Annotate `a` as an `int`, `c` as an `int`, and `b` as a `str`. + ; Equivalent to `def func(a: int, b: str = None, c: int = 1): ...` + (defn func [#^ int a #^ str [b None] #^ int [c 1]] ...) - ; Function return annotations come before the function name (if it exists) - (defn #^int add1 [#^int x] (+ x 1)) - (fn #^int [#^int y] (+ y 2)) - - The rules are: - - - The type to annotate with is the form that immediately follows the caret. - - The annotation always comes (and is evaluated) *before* the value being annotated. This is - unlike Python, where it comes and is evaluated *after* the value being annotated. + ; Function return annotations come before the function name (if + ; it exists). + (defn #^ int add1 [#^ int x] (+ x 1)) + (fn #^ int [#^ int y] (+ y 2)) For annotating items with generic types, the :hy:func:`of ` macro will likely be of use. -.. hy:function:: (annotate [value type]) - - Expanded form of :hy:data:`#^`. Syntactically equal to ``#^`` and usable wherever - you might use ``#^``:: - - (setv (annotate x int) 1) - (setv #^int x 1) ; the type comes first when using #^int - - (defn (annotate add1 int) [(annotate x int)] (+ x 1)) - + An issue with type annotations is that, as of this writing, we know of no Python type-checker that can work with :py:mod:`ast` objects or bytecode files. They all need Python source text. So you'll have to translate your Hy with ``hy2py`` in order to actually check the types. .. _dot: .. hy:data:: . - .. versionadded:: 0.10.0 - - ``.`` is used to perform attribute access on objects. It uses a small DSL - to allow quick access to attributes and items in a nested data structure. - - :strong:`Examples` + The dot macro ``.`` compiles to one or more :ref:`attribute references + `, which select an attribute of an object. The + first argument, which is required, can be an arbitrary form. With no further + arguments, ``.`` is a no-op. Additional symbol arguments are understood as a + chain of attributes, so ``(. foo bar)`` compiles to ``foo.bar``, and ``(. a b + c d)`` compiles to ``a.b.c.d``. - :: + As a convenience, ``.`` supports two other kinds of arguments in place of a + plain attribute. A parenthesized expression is understood as a method call: + ``(. foo (bar a b))`` compiles to ``foo.bar(a, b)``. A bracketed form is + understood as a subscript: ``(. foo ["bar"])`` compiles to ``foo["bar"]``. + All these options can be mixed and matched in a single ``.`` call, so :: - (. foo (bar "qux") baz [(+ 1 2)] frob) + (. a (b 1 2) c [d] [(e 3 4)]) - Compiles down to: + compiles to .. code-block:: python - foo.bar("qux").baz[1 + 2].frob - - ``.`` compiles its first argument (in the example, *foo*) as the object on - which to do the attribute dereference. It uses bare symbols as attributes - to access (in the example, *baz*, *frob*), Expressions as method calls (as in *bar*), - and compiles the contents of lists (in the example, ``[(+ 1 2)]``) for indexation. - Other arguments raise a compilation error. + a.b(1, 2).c[d][e(3, 4)] - Access to unknown attributes raises an :exc:`AttributeError`. Access to - unknown keys raises an :exc:`IndexError` (on lists and tuples) or a - :exc:`KeyError` (on dictionaries). + :ref:`Dotted identifiers ` provide syntactic sugar for + common uses of this macro. In particular, syntax like ``foo.bar`` ends up + meaning the same thing in Hy as in Python. Also, :hy:func:`get + ` is another way to subscript in Hy. -.. hy:function:: (fn [args]) +.. hy:macro:: (fn [args]) As :hy:func:`defn`, but no name for the new function is required (or - allowed), and the newly created function object is returned. Decorators - aren't allowed, either. However, the function body is understood identically - to that of :hy:func:`defn`, without any of the restrictions of Python's - :py:keyword:`lambda`. See :hy:func:`fn/a` for the asynchronous equivalent. + allowed), and the newly created function object is returned. Decorators and + type parameters aren't allowed, either. However, the function body is + understood identically to that of :hy:func:`defn`, without any of the + restrictions of Python's :py:keyword:`lambda`. See :hy:func:`fn/a` for the + asynchronous equivalent. -.. hy:function:: (fn/a [name #* args]) +.. hy:macro:: (fn/a [name #* args]) As :hy:func:`fn`, but the created function object will be a :ref:`coroutine `. -.. hy:function:: (defn [name #* args]) +.. hy:macro:: (defn [name #* args]) ``defn`` compiles to a :ref:`function definition ` (or possibly to an assignment of a :ref:`lambda expression `). It always @@ -118,20 +108,23 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. parameters (also given as symbols). Any further arguments constitute the body of the function:: - (defn name [params] bodyform1 bodyform2...) + (defn name [params] bodyform1 bodyform2…) - An empty body is implicitly ``(return None)``. If there at least two body + An empty body is implicitly ``(return None)``. If there are at least two body forms, and the first of them is a string literal, this string becomes the :term:`py:docstring` of the function. The final body form is implicitly returned; thus, ``(defn f [] 5)`` is equivalent to ``(defn f [] (return - 5))``. + 5))``. There is one exception: due to Python limitations, no implicit return + is added if the function is an asynchronous generator (i.e., defined with + :hy:func:`defn/a` or :hy:func:`fn/a` and containing at least one + :hy:func:`yield` or :hy:func:`yield-from`). - ``defn`` accepts two additional, optional arguments: a bracketed list of - :term:`decorators ` and an annotation (see :hy:data:`^`) for - the return value. These are placed before the function name (in that order, - if both are present):: + ``defn`` accepts a few more optional arguments: a bracketed list of + :term:`decorators `, a list of type parameters (see below), + and an annotation (see :hy:func:`annotate`) for the return value. These are + placed before the function name (in that order, if several are present):: - (defn [decorator1 decorator2] ^annotation name [params] …) + (defn [decorator1 decorator2] :tp [T1 T2] #^ annotation name [params] …) To define asynchronous functions, see :hy:func:`defn/a` and :hy:func:`fn/a`. @@ -175,12 +168,18 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. (print (hy.repr (f 1 2 :d 4 :e 5 :f 6))) ; => [1 2 3 4 5 {"f" 6}] -.. hy:function:: (defn/a [name lambda-list #* body]) + Type parameters require Python 3.12, and have the semantics specified by + :pep:`695`. The keyword ``:tp`` introduces the list of type parameters. Each + item of the list is a symbol, an annotated symbol (such as ``#^ int T``), or + an unpacked symbol (such as ``#* T`` or ``#** T``). As in Python, unpacking + and annotation can't be used with the same parameter. + +.. hy:macro:: (defn/a [name lambda-list #* body]) As :hy:func:`defn`, but defines a :ref:`coroutine ` like Python's ``async def``. -.. hy:function:: (defmacro [name lambda-list #* body]) +.. hy:macro:: (defmacro [name lambda-list #* body]) ``defmacro`` is used to define macros. The general format is ``(defmacro name [parameters] expr)``. @@ -203,10 +202,13 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. => (infix (1 + 1)) 2 - .. note:: because all values are passed to macros unevaluated, ``defmacro`` - cannot use keyword arguments, or kwargs. All arguments are passed - in positionally. Parameters can still be given default values - however:: + If ``defmacro`` appears in a function definition, a class definition, or a + comprehension other than :hy:func:`for` (such as :hy:func:`lfor`), the new + macro is defined locally rather than module-wide. + + .. note:: ``defmacro`` cannot use keyword arguments, because all values + are passed to macros unevaluated. All arguments are passed + positionally, but they can have default values:: => (defmacro a-macro [a [b 1]] ... `[~a ~b]) @@ -217,7 +219,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. => (a-macro :b 3) [:b 3] -.. hy:function:: (if [test then else]) +.. hy:macro:: (if [test then else]) ``if`` compiles to an :py:keyword:`if` expression (or compound ``if`` statement). The form ``test`` is evaluated and categorized as true or false according to :py:class:`bool`. If the result is true, ``then`` is evaluated and returned. Othewise, ``else`` is evaluated and returned. :: @@ -232,26 +234,19 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. - :hy:func:`when `, for shorthand for ``(if condition (do …) None)``. - :hy:func:`cond `, for shorthand for nested ``if`` forms. -.. hy:function:: (await [obj]) +.. hy:macro:: (await [obj]) ``await`` creates an :ref:`await expression `. It takes exactly one - argument: the object to wait for. - - - :strong:`Examples` + argument: the object to wait for. :: - :: - - => (import asyncio) - => (defn/a main [] - ... (print "hello") - ... (await (asyncio.sleep 1)) - ... (print "world")) - => (asyncio.run (main)) - hello - world + (import asyncio) + (defn/a main [] + (print "hello") + (await (asyncio.sleep 1)) + (print "world")) + (asyncio.run (main)) -.. hy:function:: break +.. hy:macro:: (break) ``break`` compiles to a :py:keyword:`break` statement, which terminates the enclosing loop. The following example has an infinite ``while`` loop that @@ -265,10 +260,12 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. In a loop with multiple iteration clauses, such as ``(for [x xs y ys] …)``, ``break`` only breaks out of the innermost iteration, not the whole form. To jump out of the whole form, enclose it in a :hy:func:`block - ` and use ``block-ret`` instead of ``break``, or - enclose it in a function and use :hy:func:`return`. + ` and use ``block-ret`` instead of ``break``. In + the case of :hy:func:`for`, but not :hy:func:`lfor` and the other + comprehension forms, you may also enclose it in a function and use + :hy:func:`return`. -.. hy:function:: (chainc [#* args]) +.. hy:macro:: (chainc [#* args]) ``chainc`` creates a :ref:`comparison expression `. It isn't required for unchained comparisons, which have only one comparison operator, @@ -298,7 +295,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. Python. -.. hy:function:: continue +.. hy:macro:: (continue) ``continue`` compiles to a :py:keyword:`continue` statement, which returns execution to the start of a loop. In the following example, ``(.append @@ -321,24 +318,31 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. in ``(for [x xs] (block (for [y ys] …)))``. You can then use ``block-ret`` in place of ``continue``. -.. hy:function:: (do [#* body]) +.. hy:macro:: (do [#* body]) ``do`` (called ``progn`` in some Lisps) takes any number of forms, evaluates them, and returns the value of the last one, or ``None`` if no - forms were provided. + forms were provided. :: - :strong:`Examples` + (+ 1 (do (setv x (+ 1 1)) x)) ; => 3 - :: +.. hy:macro:: (do-mac [#* body]) - => (+ 1 (do (setv x (+ 1 1)) x)) - 3 + ``do-mac`` evaluates its arguments (in order) at compile time, and leaves behind the value of the last argument (``None`` if no arguments were provided) as code to be run. The effect is similar to defining and then immediately calling a nullary macro, hence the name, which stands for "do macro". :: + + (do-mac `(setv ~(hy.models.Symbol (* "x" 5)) "foo")) + ; Expands to: (setv xxxxx "foo") + (print xxxxx) + ; => "foo" + + Contrast with :hy:func:`eval-and-compile`, which evaluates the same code at compile-time and run-time, instead of using the result of the compile-time run as code for run-time. ``do-mac`` is also similar to Common Lisp's SHARPSIGN DOT syntax (``#.``), from which it differs by evaluating at compile-time rather than read-time. -.. hy:function:: (for [#* args]) +.. hy:macro:: (for [#* args]) - ``for`` is used to evaluate some forms for each element in an iterable - object, such as a list. The return values of the forms are discarded and - the ``for`` form returns ``None``. + ``for`` compiles to one or more :py:keyword:`for` statements, which + execute code repeatedly for each element of an iterable object. + The return values of the forms are discarded and the ``for`` form + returns ``None``. :: @@ -352,10 +356,10 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. iterating 3 - In its square-bracketed first argument, ``for`` allows the same types of - clauses as :hy:func:`lfor`. + The first argument of ``for``, in square brackets, specifies how to loop. A simple and common case is ``[variable values]``, where ``values`` is a form that evaluates to an iterable object (such as a list) and ``variable`` is a symbol specifiying the name to assign each element to. Subsequent arguments to ``for`` are body forms to be evaluated for each iteration of the loop. - :: + More generally, the first argument of ``for`` allows the same types of + clauses as :hy:func:`lfor`:: => (for [x [1 2 3] :if (!= x 2) y [7 8]] ... (print x y)) @@ -364,7 +368,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. 3 7 3 8 - Furthermore, the last argument of ``for`` can be an ``(else …)`` form. + The last argument of ``for`` can be an ``(else …)`` form. This form is executed after the last iteration of the ``for``\'s outermost iteration clause, but only if that outermost loop terminates normally. If it's jumped out of with e.g. ``break``, the ``else`` is @@ -388,87 +392,40 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. 3 loop finished -.. hy:function:: (assert [condition [label None]]) - - ``assert`` is used to verify conditions while the program is - running. If the condition is not met, an :exc:`AssertionError` is - raised. ``assert`` may take one or two parameters. The first - parameter is the condition to check, and it should evaluate to either - ``True`` or ``False``. The second parameter, optional, is a label for - the assert, and is the string that will be raised with the - :exc:`AssertionError`. For example: - - :strong:`Examples` - - :: - - (assert (= variable expected-value)) +.. hy:macro:: (assert [condition [label None]]) - (assert False) - ; AssertionError + ``assert`` compiles to an :py:keyword:`assert` statement, which checks + whether a condition is true. The first argument, specifying the condition to + check, is mandatory, whereas the second, which will be passed to + :py:class:`AssertionError`, is optional. The whole form is only evaluated + when :py:data:`__debug__` is true, and the second argument is only evaluated + when :py:data:`__debug__` is true and the condition fails. ``assert`` always + returns ``None``. :: (assert (= 1 2) "one should equal two") - ; AssertionError: one should equal two + ; AssertionError: one should equal two -.. hy:function:: (global [sym]) +.. hy:macro:: (global [#* syms]) - ``global`` can be used to mark a symbol as global. This allows the programmer to - assign a value to a global symbol. Reading a global symbol does not require the - ``global`` keyword -- only assigning it does. + ``global`` compiles to a :py:keyword:`global` statement, which declares one + or more names as referring to global (i.e., module-level) variables. The + arguments are symbols; with no arguments, ``global`` has no effect. The + return value is always ``None``. :: - The following example shows how the global symbol ``a`` is assigned a value in a - function and is later on printed in another function. Without the ``global`` - keyword, the second function would have raised a ``NameError``. - - :strong:`Examples` - - :: - - (defn set-a [value] + (setv a 1 b 10) + (print a b) ; => 1 10 + (defn f [] (global a) - (setv a value)) - - (defn print-a [] - (print a)) - - (set-a 5) - (print-a) + (setv a 2 b 20)) + (f) + (print a b) ; => 2 10 -.. hy:function:: (get [coll key1 #* keys]) - ``get`` is used to access single elements in collections. ``get`` takes at - least two parameters: the *data structure* and the *index* or *key* of the - item. It will then return the corresponding value from the collection. If - multiple *index* or *key* values are provided, they are used to access - successive elements in a nested structure. Example usage: - - :strong:`Examples` - - :: - - => (do - ... (setv animals {"dog" "bark" "cat" "meow"} - ... numbers #("zero" "one" "two" "three") - ... nested [0 1 ["a" "b" "c"] 3 4]) - ... (print (get animals "dog")) - ... (print (get numbers 2)) - ... (print (get nested 2 1))) - - bark - two - b - - .. note:: ``get`` raises a KeyError if a dictionary is queried for a - non-existing key. - - .. note:: ``get`` raises an IndexError if a list or a tuple is queried for an - index that is out of bounds. - -.. hy:function:: (import [#* forms]) +.. hy:macro:: (import [#* forms]) ``import`` compiles to an :py:keyword:`import` statement, which makes objects - in a different module available in the current module. Hy's syntax for the - various kinds of import looks like this:: + in a different module available in the current module. It always returns + ``None``. Hy's syntax for the various kinds of import looks like this:: ;; Import each of these modules ;; Python: import sys, os.path @@ -502,9 +459,26 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. `. The macro :hy:func:`export ` is a handy way to set ``__all__`` in a Hy program. -.. hy:function:: (eval-and-compile [#* body]) +.. hy:macro:: (eval-and-compile [#* body]) + + ``eval-and-compile`` takes any number of forms as arguments. The input forms are evaluated as soon as the ``eval-and-compile`` form is compiled, then left in the program so they can be executed at run-time as usual; contrast with :hy:func:`eval-when-compile`. So, if you compile and immediately execute a program (as calling ``hy foo.hy`` does when ``foo.hy`` doesn't have an up-to-date byte-compiled version), ``eval-and-compile`` forms will be evaluated twice. For example, the following program :: + + (eval-when-compile + (print "Compiling")) + (print "Running") + (eval-and-compile + (print "Hi")) + + prints + + .. code-block:: text + + Compiling + Hi + Running + Hi - ``eval-and-compile`` is a special form that takes any number of forms. The input forms are evaluated as soon as the ``eval-and-compile`` form is compiled, instead of being deferred until run-time. The input forms are also left in the program so they can be executed at run-time as usual. So, if you compile and immediately execute a program (as calling ``hy foo.hy`` does when ``foo.hy`` doesn't have an up-to-date byte-compiled version), ``eval-and-compile`` forms will be evaluated twice. + The return value of ``eval-and-compile`` is its final argument, as for :hy:func:`do`. One possible use of ``eval-and-compile`` is to make a function available both at compile-time (so a macro can call it while expanding) and run-time (so it can be called like any other function):: @@ -520,13 +494,35 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. Had the ``defn`` not been wrapped in ``eval-and-compile``, ``m`` wouldn't be able to call ``add``, because when the compiler was expanding ``(m 3)``, ``add`` wouldn't exist yet. -.. hy:function:: (eval-when-compile [#* body]) + While ``eval-and-compile`` executes the same code at both compile-time and run-time, bear in mind that the same code can have different meanings in the two contexts. Consider, for example, issues of scoping:: + + (eval-when-compile + (print "Compiling")) + (print "Running") + (eval-and-compile + (setv x 1)) + (defn f [] + (setv x 2) + (eval-and-compile + (setv x 3)) + (print "local x =" x)) + (f) + (eval-and-compile + (print "global x =" x)) + + The form ``(setv x 3)`` above refers to the global ``x`` at compile-time, but the local ``x`` at run-time, so the result is: - ``eval-when-compile`` is like ``eval-and-compile``, but the code isn't executed at run-time. Hence, ``eval-when-compile`` doesn't directly contribute any code to the final program, although it can still change Hy's state while compiling (e.g., by defining a function). + .. code-block:: text - :strong:`Examples` + Compiling + global x = 3 + Running + local x = 3 + global x = 1 - :: +.. hy:macro:: (eval-when-compile [#* body]) + + ``eval-when-compile`` executes the given forms at compile-time, but discards them at run-time and simply returns :data:`None` instead; contrast :hy:func:`eval-and-compile`. Hence, while ``eval-when-compile`` doesn't directly contribute code to the final program, it can change Hy's state while compiling, as by defining a function:: (eval-when-compile (defn add [x y] @@ -538,15 +534,14 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. (print (m 3)) ; prints 5 (print (add 3 6)) ; raises NameError: name 'add' is not defined -.. hy:function:: (lfor [#* args]) +.. hy:macro:: (lfor [#* args]) The comprehension forms ``lfor``, :hy:func:`sfor`, :hy:func:`dfor`, :hy:func:`gfor`, and :hy:func:`for` are used to produce various kinds of loops, including Python-style :ref:`comprehensions `. ``lfor`` in particular - creates a list comprehension. A simple use of ``lfor`` is:: + can create a list comprehension. A simple use of ``lfor`` is:: - => (lfor x (range 5) (* 2 x)) - [0 2 4 6 8] + (lfor x (range 5) (* 2 x)) ; => [0 2 4 6 8] ``x`` is the name of a new variable, which is bound to each element of ``(range 5)``. Each such element in turn is used to evaluate the value @@ -554,13 +549,13 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. Here's a more complex example:: - => (lfor - ... x (range 3) - ... y (range 3) - ... :if (!= x y) - ... :setv total (+ x y) - ... [x y total]) - [[0 1 1] [0 2 2] [1 0 1] [1 2 3] [2 0 2] [2 1 3]] + (lfor + x (range 3) + y (range 3) + :if (!= x y) + :setv total (+ x y) + [x y total]) + ; => [[0 1 1] [0 2 2] [1 0 1] [1 2 3] [2 0 2] [2 1 3]] When there are several iteration clauses (here, the pairs of forms ``x (range 3)`` and ``y (range 3)``), the result works like a nested loop or @@ -590,12 +585,12 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. For ``lfor``, ``sfor``, ``gfor``, and ``dfor``, variables defined by an iteration clause or ``:setv`` are not visible outside the form. - However, variables defined within the body, such as via a ``setx`` + However, variables defined within the body, as with a ``setx`` expression, will be visible outside the form. - By contrast, iteration and ``:setv`` clauses for ``for`` share the + In ``for``, by contrast, iteration and ``:setv`` clauses share the caller's scope and are visible outside the form. -.. hy:function:: (dfor [#* args]) +.. hy:macro:: (dfor [#* args]) ``dfor`` creates a :ref:`dictionary comprehension `. Its syntax is the same as that of :hy:func:`lfor` except that it takes two trailing @@ -606,15 +601,11 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. {0 0 1 10 2 20 3 30 4 40} -.. hy:function:: (gfor [#* args]) +.. hy:macro:: (gfor [#* args]) ``gfor`` creates a :ref:`generator expression `. Its syntax is the same as that of :hy:func:`lfor`. The difference is that ``gfor`` returns - an iterator, which evaluates and yields values one at a time. - - :strong:`Examples` - - :: + an iterator, which evaluates and yields values one at a time:: => (import itertools [count take-while]) => (setv accum []) @@ -625,59 +616,44 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. => accum [0 1 2 3 4 5] -.. hy:function:: (sfor [#* args]) +.. hy:macro:: (sfor [#* args]) - ``sfor`` creates a set comprehension. ``(sfor CLAUSES VALUE)`` is + ``sfor`` creates a :ref:`set comprehension `. ``(sfor CLAUSES VALUE)`` is equivalent to ``(set (lfor CLAUSES VALUE))``. See :hy:func:`lfor`. -.. hy:function:: (setv [#* args]) - - ``setv`` is used to bind a value, object, or function to a symbol. - - :strong:`Examples` - - :: - - => (setv names ["Alice" "Bob" "Charlie"]) - => (print names) - ['Alice', 'Bob', 'Charlie'] +.. hy:macro:: (setv [#* args]) - => (setv counter (fn [collection item] (.count collection item))) - => (counter [1 2 3 4 5 2 3] 2) - 2 + ``setv`` compiles to an :ref:`assignment statement ` (see :hy:func:`setx` for assignment expressions), which sets the value of a variable or some other assignable expression. It requires an even number of arguments, and always returns ``None``. The most common case is two arguments, where the first is a symbol:: - You can provide more than one target–value pair, and the assignments will be made in order:: + (setv websites 103) + (print websites) ; => 103 - => (setv x 1 y x x 2) - => (print x y) - 2 1 + Additional pairs of arguments are equivalent to several two-argument ``setv`` calls, in the given order. Thus, the semantics are like Common Lisp's ``setf`` rather than ``psetf``. :: - You can perform parallel assignments or unpack the source value with square brackets and :hy:func:`unpack-iterable `:: + (setv x 1 y x x 2) + (print x y) ; => 2 1 - => (setv duo ["tim" "eric"]) - => (setv [guy1 guy2] duo) - => (print guy1 guy2) - tim eric + All the same kinds of complex assignment targets are allowed as in Python. So, you can use list assignment to assign in parallel. (As in Python, tuple and list syntax are equivalent for this purpose; Hy differs from Python merely in that its list syntax is shorter than its tuple syntax.) :: - => (setv [letter1 letter2 #* others] "abcdefg") - => (print letter1 letter2 others) - a b ['c', 'd', 'e', 'f', 'g'] + (setv [x y] [y x]) ; Swaps the values of `x` and `y` + Unpacking assignment looks like this (see :hy:func:`unpack-iterable`):: -.. hy:function:: (setx [#* args]) + (setv [letter1 letter2 #* others] "abcdefg") + (print letter1 letter2 (hy.repr others)) + ; => a b ["c" "d" "e" "f" "g"] - Whereas ``setv`` creates an assignment statement, ``setx`` creates an assignment expression (see :pep:`572`). It requires Python 3.8 or later. Only one target–value pair is allowed, and the target must be a bare symbol, but the ``setx`` form returns the assigned value instead of ``None``. + See :hy:func:`let` to simulate more traditionally Lispy block-level scoping. - :strong:`Examples` +.. hy:macro:: (setx [target value]) - :: + ``setx`` compiles to an assignment expression. Thus, unlike :hy:func:`setv`, it returns the assigned value. It takes exactly two arguments, and the target must be a bare symbol. Python 3.8 or later is required. :: - => (when (> (setx x (+ 1 2)) 0) - ... (print x "is greater than 0")) - 3 is greater than 0 + (when (> (setx x (+ 1 2)) 0) + (print x "is greater than 0")) + ; => 3 is greater than 0 - -.. hy:function:: (let [bindings #* body]) +.. hy:macro:: (let [bindings #* body]) ``let`` creates lexically-scoped names for local variables. This form takes a list of binding pairs followed by a *body* which gets executed. A let-bound @@ -743,19 +719,19 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. .. _extended iterable unpacking: https://www.python.org/dev/peps/pep-3132/#specification -.. hy:function:: (match [subject #* cases]) +.. hy:macro:: (match [subject #* cases]) The ``match`` form creates a :ref:`match statement `. It requires Python 3.10 or later. The first argument should be the subject, and any remaining arguments should be pairs of patterns and results. The ``match`` form returns the value of the corresponding result, or - ``None`` if no case matched. For example:: + ``None`` if no case matched. :: - => (match (+ 1 1) - ... 1 "one" - ... 2 "two" - ... 3 "three") - "two" + (match (+ 1 1) + 1 "one" + 2 "two" + 3 "three") + ; => "two" You can use :hy:func:`do` to build a complex result form. Patterns, as in Python match statements, are interpreted specially and can't be @@ -764,123 +740,85 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. value, sequence, mapping, and class patterns. Guards are specified with ``:if FORM``. Here's a more complex example:: - => (match #(100 200) - ... [100 300] "Case 1" - ... [100 200] :if flag "Case 2" - ... [900 y] f"Case 3, y: {y}" - ... [100 (| 100 200) :as y] f"Case 4, y: {y}" - ... _ "Case 5, I match anything!") + (match #(100 200) + [100 300] "Case 1" + [100 200] :if flag "Case 2" + [900 y] f"Case 3, y: {y}" + [100 (| 100 200) :as y] f"Case 4, y: {y}" + _ "Case 5, I match anything!") This will match case 2 if ``flag`` is true and case 4 otherwise. ``match`` can also match against class instances by keyword (or - positionally if its ``__match_args__`` attribute is defined, see - `pep 636 `_):: - - => (import dataclasses [dataclass]) - => (defclass [dataclass] Point [] - ... #^int x - ... #^int y) - => (match (Point 1 2) - ... (Point 1 x) :if (= (% x 2) 0) x) - 2 - -.. hy:function:: (defclass [class-name super-classes #* body]) - - New classes are declared with ``defclass``. It can take optional parameters - in the following order: a list defining (a) possible super class(es) and a - string (:term:`py:docstring`). The class name may also be preceded by a list - of :term:`decorators `, as in :hy:func:`defn`. - - :strong:`Examples` - - :: - - => (defclass class-name [super-class-1 super-class-2] - ... "docstring" - ... - ... (setv attribute1 value1) - ... (setv attribute2 value2) - ... - ... (defn method [self] (print "hello!"))) - - Both values and functions can be bound on the new class as shown by the example - below: - - :: - - => (defclass Cat [] - ... (setv age None) - ... (setv colour "white") - ... - ... (defn speak [self] (print "Meow"))) - - => (setv spot (Cat)) - => (setv spot.colour "Black") - => (.speak spot) - Meow - -.. hy:function:: (del [object]) - - .. versionadded:: 0.9.12 - - ``del`` removes an object from the current namespace. - - :strong:`Examples` - - :: - - => (setv foo 42) - => (del foo) - => foo - Traceback (most recent call last): - File "", line 1, in - NameError: name 'foo' is not defined - - ``del`` can also remove objects from mappings, lists, and more. - - :: - - => (setv test (list (range 10))) - => test - [0 1 2 3 4 5 6 7 8 9] - => (del (cut test 2 4)) ;; remove items from 2 to 4 excluded - => test - [0 1 4 5 6 7 8 9] - => (setv dic {"foo" "bar"}) - => dic - {"foo" "bar"} - => (del (get dic "foo")) - => dic - {} - -.. hy:function:: (nonlocal [object]) - - .. versionadded:: 0.11.1 - - ``nonlocal`` can be used to mark a symbol as not local to the current scope. - The parameters are the names of symbols to mark as nonlocal. This is necessary - to modify variables through nested ``fn`` scopes: - - :strong:`Examples` - - :: - - (defn some-function [] - (setv x 0) - (register-some-callback - (fn [stuff] - (nonlocal x) - (setv x stuff)))) - - Without the call to ``(nonlocal x)``, the inner function would redefine ``x`` to - ``stuff`` inside its local scope instead of overwriting the ``x`` in the outer - function. - - See `PEP3104 `_ for further - information. - -.. hy:function:: (py [string]) + positionally if its ``__match_args__`` attribute is defined; see :pep:`636`):: + + (import dataclasses [dataclass]) + (defclass [dataclass] Point [] + #^ int x + #^ int y) + (match (Point 1 2) + (Point 1 x) :if (= (% x 2) 0) x) ; => 2 + + It's worth emphasizing that ``match`` is a pattern-matching construct + rather than a generic `switch + `_ construct, and + retains all of Python's limitations on match patterns. For example, you + can't match against the value of a variable. For more flexible branching + constructs, see Hyrule's :hy:func:`branch ` and + :hy:func:`case `, or simply use :hy:func:`cond + `. + +.. hy:macro:: (defclass [arg1 #* args]) + + ``defclass`` compiles to a :py:keyword:`class` statement, which creates a + new class. It always returns ``None``. Only one argument, specifying the + name of the new class as a symbol, is required. A list of :term:`decorators + ` (and type parameters, in the same way as for + :hy:func:`defn`) may be provided before the class name. After the name comes + a list of superclasses (use the empty list ``[]`` for the typical case of no + superclasses) and any number of body forms, the first of which may be a + :term:`py:docstring`. :: + + (defclass [decorator1 decorator2] :tp [T1 T2] MyClass [SuperClass1 SuperClass2] + "A class that does things at times." + + (setv + attribute1 value1 + attribute2 value2) + + (defn method1 [self arg1 arg2] + …) + + (defn method2 [self arg1 arg2] + …)) + +.. hy:macro:: (del [#* args]) + + ``del`` compiles to a :py:keyword:`del` statement, which deletes variables + or other assignable expressions. It always returns ``None``. :: + + (del foo (get mydict "mykey") myobj.myattr) + +.. hy:macro:: (nonlocal [#* syms]) + + Similar to :hy:func:`global`, but names can be declared in any enclosing + scope. ``nonlocal`` compiles to a :py:keyword:`global` statement for any + names originally defined in the global scope, and a :py:keyword:`nonlocal` + statement for all other names. :: + + (setv a 1 b 1) + (defn f [] + (setv c 10 d 10) + (defn g [] + (nonlocal a c) + (setv a 2 b 2 + c 20 d 20)) + (print a b c d) ; => 1 1 10 10 + (g) + (print a b c d)) ; => 2 1 20 10 + (f) + +.. hy:macro:: (py [string]) ``py`` parses the given Python code at compile-time and inserts the result into the generated abstract syntax tree. Thus, you can mix Python code into a Hy @@ -893,71 +831,95 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. The code must be given as a single string literal, but you can still use macros, :hy:func:`hy.eval `, and related tools to construct the ``py`` form. If having to backslash-escape internal double quotes is getting you down, try a - :ref:`bracket string `. If you want to evaluate some - Python code that's only defined at run-time, try the standard Python function + :ref:`bracket string `. If you want to evaluate some Python + code that's only defined at run-time, try the standard Python function :func:`eval`. + The code is implicitly wrapped in parentheses so Python won't give you grief + about indentation. After all, Python's indentation rules are only useful for + grouping statements, whereas ``py`` only allows an expression. + Python code need not syntactically round-trip if you use ``hy2py`` on a Hy program that uses ``py`` or ``pys``. For example, comments will be removed. - - .. _pys-specialform: - -.. hy:function:: (pys [string]) +.. hy:macro:: (pys [string]) As :hy:func:`py `, but the code can consist of zero or more statements, including compound statements such as ``for`` and ``def``. ``pys`` always - returns ``None``. Also, the code string is dedented with - :func:`textwrap.dedent` before parsing, which allows you to intend the code to - match the surrounding Hy code, but significant leading whitespace in embedded - string literals will be removed. :: + returns ``None``. :: (pys "myvar = 5") (print "myvar is" myvar) -.. hy:function:: (quasiquote [form]) + Unlike ``py``, no parentheses are added, because Python doesn't allow + statements to be parenthesized. Instead, the code string is dedented with + :func:`textwrap.dedent` before parsing. Thus you can indent the code to + match the surrounding Hy code when Python would otherwise forbid this, but + beware that significant leading whitespace in embedded string literals will + be removed. - ``quasiquote`` allows you to quote a form, but also selectively evaluate - expressions. Expressions inside a ``quasiquote`` can be selectively evaluated - using ``unquote`` (``~``). The evaluated form can also be spliced using - ``unquote-splice`` (``~@``). Quasiquote can be also written using the backquote - (`````) symbol. +.. hy:macro:: (quasiquote [model]) +.. hy:macro:: (unquote [model]) +.. hy:macro:: (unquote-splice [model]) - :strong:`Examples` + ``quasiquote`` is like :hy:func:`quote` except that it treats the model as a template, in which certain special :ref:`expressions ` indicate that some code should be evaluated and its value substituted there. The idea is similar to C's ``sprintf`` or Python's various string-formatting constructs. For example:: - :: + (setv x 2) + (quasiquote (+ 1 (unquote x))) ; => '(+ 1 2) - ;; let `qux' be a variable with value (bar baz) - `(foo ~qux) - ; equivalent to '(foo (bar baz)) - `(foo ~@qux) - ; equivalent to '(foo bar baz) + ``unquote`` indicates code to be evaluated, so ``x`` becomes ``2`` and the ``2`` gets inserted in the parent model. ``quasiquote`` can be :ref:`abbreviated ` as a backtick (\`), with no parentheses, and likewise ``unquote`` can be abbreviated as a tilde (``~``), so one can instead write simply :: + `(+ 1 ~x) -.. hy:function:: (quote [form]) + (In the bulk of Lisp tradition, unquotation is written ``,``. Hy goes with Clojure's choice of ``~``, which has the advantage of being more visible in most programming fonts.) - ``quote`` returns the form passed to it without evaluating it. ``quote`` can - alternatively be written using the apostrophe (``'``) symbol. + Quasiquotation is convenient for writing macros:: - :strong:`Examples` + (defmacro set-foo [value] + `(setv foo ~value)) + (set-foo (+ 1 2 3)) + (print foo) ; => 6 - :: + Another kind of unquotation operator, ``unquote-splice``, abbreviated ``~@``, is analogous to ``unpack-iterable`` in that it splices an iterable object into the sequence of the parent :ref:`sequential model `. Compare the effects of ``unquote`` to ``unquote-splice``:: - => (setv x '(print "Hello World")) - => x ; variable x is set to unevaluated expression - hy.models.Expression([ - hy.models.Symbol('print'), - hy.models.String('Hello World')]) - => (hy.eval x) - Hello World + (setv X [1 2 3]) + (hy.repr `[a b ~X c d ~@X e f]) + ; => '[a b [1 2 3] c d 1 2 3 e f] + If ``unquote-splice`` is given any sort of false value (such as ``None``), it's treated as an empty list. To be precise, ``~@x`` splices in the result of ``(or x [])``. -.. hy:function:: (require [#* args]) + Note that while a symbol name can begin with ``@`` in Hy, ``~@`` takes precedence in the parser, so if you want to unquote the symbol ``@foo`` with ``~``, you must use whitespace to separate ``~`` and ``@``, as in ``~ @foo``. + +.. hy:macro:: (quote [model]) + + Return the given :ref:`model ` without evaluating it. Or to be more pedantic, ``quote`` complies to code that produces and returns the model it was originally called on. Thus ``quote`` serves as syntactic sugar for model constructors:: + + (quote a) + ; Equivalent to: (hy.models.Symbol "a") + (quote (+ 1 1)) + ; Equivalent to: (hy.models.Expression [ + ; (hy.models.Symbol "+") + ; (hy.models.Integer 1) + ; (hy.models.Integer 1)]) + + ``quote`` itself is conveniently :ref:`abbreviated ` as the single-quote character ``'``, which needs no parentheses, allowing one to instead write:: + + 'a + '(+ 1 1) + + See also: + + - :hy:func:`quasiquote` to substitute values into a quoted form + - :hy:func:`hy.eval` to evaluate models as code + - :hy:func:`hy.repr` to stringify models into Hy source text that uses ``'`` + +.. hy:macro:: (require [#* args]) ``require`` is used to import macros and reader macros from one or more given modules. It allows parameters in all the same formats as ``import``. ``require`` imports each named module and then makes each requested macro - available in the current module. + available in the current module, or in the current local scope if called + locally (using the same notion of locality as :hy:func:`defmacro`). The following are all equivalent ways to call a macro named ``foo`` in the module ``mymodule``. @@ -1001,8 +963,8 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. NameError: name 'mymodule' is not defined Unlike requiring regular macros, reader macros cannot be renamed - with ``:as``, and are not made available under their absolute paths - to their source module:: + with ``:as``, are not made available under their absolute paths + to their source module, and can't be required locally:: => (require mymodule :readers [!]) HySyntaxError: ... @@ -1078,132 +1040,92 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. to do introspection of the current module's set of defined macros, which isn't really supported anyway. -.. hy:function:: (return [object]) +.. hy:macro:: (return [object]) ``return`` compiles to a :py:keyword:`return` statement. It exits the - current function, returning its argument if provided with one or - ``None`` if not. - - :strong:`Examples` - - :: - - => (defn f [x] (for [n (range 10)] (when (> n x) (return n)))) - => (f 3.9) - 4 - - Note that in Hy, ``return`` is necessary much less often than in Python, - since the last form of a function is returned automatically. Hence, an - explicit ``return`` is only necessary to exit a function early. - - :: - - => (defn f [x] (setv y 10) (+ x y)) - => (f 4) - 14 - - To get Python's behavior of returning ``None`` when execution reaches - the end of a function, put ``None`` there yourself. - - :: - - => (defn f [x] (setv y 10) (+ x y) None) - => (print (f 4)) - None - -.. hy:function:: (cut [coll [start None] [stop None] [step None]) - - ``cut`` can be used to take a subset of a list and create a new list from it. - The form takes at least one parameter specifying the list to cut. Two - optional parameters can be used to give the start and end position of the - subset. If only one is given, it is taken as the ``stop`` value. - The third optional parameter is used to control the step stride between the elements. - - ``cut`` follows the same rules as its Python counterpart. Negative indices are - counted starting from the end of the list. Some example usage: - - :strong:`Examples` - - :: - - => (setv collection (range 10)) - => (cut collection) - [0 1 2 3 4 5 6 7 8 9] - - => (cut collection 5) - [0 1 2 3 4] - - => (cut collection 2 8) - [2 3 4 5 6 7] - - => (cut collection 2 8 2) - [2 4 6] - - => (cut collection -4 -2) - [6 7] - -.. hy:function:: (raise [[exception None]]) - - The ``raise`` form can be used to raise an ``Exception`` at - runtime. Example usage: - - :strong:`Examples` - - :: - - (raise) - ; re-rase the last exception - - (raise IOError) - ; raise an IOError - - (raise (IOError "foobar")) - ; raise an IOError("foobar") - - - ``raise`` can accept a single argument (an ``Exception`` class or instance) - or no arguments to re-raise the last ``Exception``. - - -.. hy:function:: (try [#* body]) - - The ``try`` form is used to catch exceptions (``except``) and run cleanup - actions (``finally``). - - :strong:`Examples` - - :: - - (try - (error-prone-function) - (another-error-prone-function) - (except [ZeroDivisionError] - (print "Division by zero")) - (except [[IndexError KeyboardInterrupt]] - (print "Index error or Ctrl-C")) - (except [e ValueError] - (print "ValueError:" (repr e))) - (except [e [TabError PermissionError ReferenceError]] - (print "Some sort of error:" (repr e))) - (else - (print "No errors")) - (finally - (print "All done"))) - - The first argument of ``try`` is its body, which can contain one or more forms. - Then comes any number of ``except`` clauses, then optionally an ``else`` - clause, then optionally a ``finally`` clause. If an exception is raised with a - matching ``except`` clause during the execution of the body, that ``except`` - clause will be executed. If no exceptions are raised, the ``else`` clause is - executed. The ``finally`` clause will be executed last regardless of whether an - exception was raised. - - The return value of ``try`` is the last form of the ``except`` clause that was - run, or the last form of ``else`` if no exception was raised, or the ``try`` - body if there is no ``else`` clause. - -.. hy:function:: (unpack-iterable) -.. hy:function:: (unpack-mapping) + current function, returning its argument if provided with one, or + ``None`` if not. :: + + (defn f [x] + (for [n (range 10)] + (when (> n x) + (return n)))) + (f 3.9) ; => 4 + + Note that in Hy, ``return`` is necessary much less often than in Python. + The last form of a function is returned automatically, so an + explicit ``return`` is only necessary to exit a function early. To force + Python's behavior of returning ``None`` when execution reaches the end of a + function, just put ``None`` there yourself:: + + (defn f [] + (setv d (dict :a 1 :b 2)) + (.pop d "b") + None) + (print (f)) ; Prints "None", not "2" + +.. hy:macro:: (raise [exception :from other]) + + ``raise`` compiles to a :py:keyword:`raise` statement, which throws an + exception. With no arguments, the current exception is reraised. With one + argument, an exception, that exception is raised. :: + + (try + (raise KeyError) + (except [KeyError] + (print "gottem"))) + + ``raise`` supports one other syntax, ``(raise EXCEPTION_1 :from + EXCEPTION_2)``, which compiles to a Python ``raise … from`` statement like + ``raise EXCEPTION_1 from EXCEPTION_2``. + +.. hy:macro:: (try [#* body]) + + ``try`` compiles to a :py:keyword:`try` statement, which can catch + exceptions and run cleanup actions. It begins with any number of body forms. + Then follows any number of ``except`` or ``except*`` (:pep:`654`) forms, + which are expressions that begin with the symbol in question, followed by a + list of exception types, followed by more body forms. Finally there are an + optional ``else`` form and an optional ``finally`` form, which again are + expressions that begin with the symbol in question and then comprise body + forms. Note that ``except*`` requires Python 3.11, and ``except*`` and + ``except`` may not both be used in the same ``try``. + + Here's an example of several of the allowed kinds of child forms:: + + (try + (error-prone-function) + (another-error-prone-function) + (except [ZeroDivisionError] + (print "Division by zero")) + (except [[IndexError KeyboardInterrupt]] + (print "Index error or Ctrl-C")) + (except [e ValueError] + (print "ValueError:" (repr e))) + (except [e [TabError PermissionError ReferenceError]] + (print "Some sort of error:" (repr e))) + (else + (print "No errors")) + (finally + (print "All done"))) + + Exception lists can be in any of several formats: + + - ``[]`` to catch any subtype of ``Exception``, like Python's ``except:`` + - ``[ETYPE]`` to catch only the single type ``ETYPE``, like Python's + ``except ETYPE:`` + - ``[[ETYPE1 ETYPE2 …]]`` to catch any of the named types, like Python's + ``except ETYPE1, ETYPE2, …:`` + - ``[VAR ETYPE]`` to catch ``ETYPE`` and bind it to ``VAR``, like Python's + ``except ETYPE as VAR:`` + - ``[VAR [ETYPE1 ETYPE2 …]]`` to catch any of the named types and bind it to + ``VAR``, like Python's ``except ETYPE1, ETYPE2, … as VAR:`` + + The return value of ``try`` is the last form evaluated among the main body, + ``except`` forms, ``except*`` forms, and ``else``. + +.. hy:macro:: (unpack-iterable [form]) +.. hy:macro:: (unpack-mapping [form]) (Also known as the splat operator, star operator, argument expansion, argument explosion, argument gathering, and varargs, among others...) @@ -1241,62 +1163,19 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. => (f #* [1] #* [2] #** {"c" 3} #** {"d" 4}) [1 2 3 4] -.. hy:function:: (unquote [symbol]) - - Within a quasiquoted form, ``unquote`` forces evaluation of a symbol. ``unquote`` - is aliased to the tilde (``~``) symbol. - - :: - - => (setv nickname "Cuddles") - => (quasiquote (= nickname (unquote nickname))) - '(= nickname "Cuddles") - => `(= nickname ~nickname) - '(= nickname "Cuddles") - - -.. hy:function:: (unquote-splice [symbol]) - - ``unquote-splice`` forces the evaluation of a symbol within a quasiquoted form, - much like ``unquote``. ``unquote-splice`` can be used when the symbol - being unquoted contains an iterable value, as it "splices" that iterable into - the quasiquoted form. ``unquote-splice`` can also be used when the value - evaluates to a false value such as ``None``, ``False``, or ``0``, in which - case the value is treated as an empty list and thus does not splice anything - into the form. ``unquote-splice`` is aliased to the ``~@`` syntax. - - :: - - => (setv nums [1 2 3 4]) - => (quasiquote (+ (unquote-splice nums))) - '(+ 1 2 3 4) - => `(+ ~@nums) - '(+ 1 2 3 4) - => `[1 2 ~@(when (< (get nums 0) 0) nums)] - '[1 2] - - Here, the last example evaluates to ``('+' 1 2)``, since the condition - ``(< (nth nums 0) 0)`` is ``False``, which makes this ``if`` expression - evaluate to ``None``, because the ``if`` expression here does not have an - else clause. ``unquote-splice`` then evaluates this as an empty value, - leaving no effects on the list it is enclosed in, therefore resulting in - ``('+' 1 2)``. - -.. hy:function:: (while [condition #* body]) - - ``while`` compiles to a :py:keyword:`while` statement. It is used to execute a - set of forms as long as a condition is met. The first argument to ``while`` is - the condition, and any remaining forms constitute the body. The following - example will output "Hello world!" to the screen indefinitely: - - :: +.. hy:macro:: (while [condition #* body]) - (while True (print "Hello world!")) + ``while`` compiles to a :py:keyword:`while` statement, which executes some + code as long as a condition is met. The first argument to ``while`` is the + condition, and any remaining forms constitute the body. It always returns + ``None``. :: - The last form of a ``while`` loop can be an ``else`` clause, which is executed - after the loop terminates, unless it exited abnormally (e.g., with ``break``). So, + (while True + (print "Hello world!")) - :: + The last form of a ``while`` loop can be an ``else`` clause, which is + executed after the loop terminates, unless it exited abnormally (e.g., with + ``break``). So, :: (setv x 2) (while x @@ -1305,9 +1184,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. (else (print "In else"))) - prints - - :: + prints :: In body In body @@ -1317,9 +1194,7 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. loop, it will apply to the very same loop rather than an outer loop, even if execution is yet to ever reach the loop body. (Hy compiles a ``while`` loop with statements in its condition by rewriting it so that the condition is - actually in the body.) So, - - :: + actually in the body.) So, :: (for [x [1]] (print "In outer loop") @@ -1332,102 +1207,118 @@ base names, such that ``hy.core.macros.foo`` can be called as just ``foo``. (print "This won't print, either.")) (print "At end of outer loop")) - prints - - :: + prints :: In outer loop In condition At end of outer loop -.. hy:function:: (with [#* args]) +.. hy:macro:: (with [managers #* body]) - Wrap execution of `body` within a context manager given as bracket `args`. - ``with`` is used to wrap the execution of a block within a context manager. The - context manager can then set up the local system and tear it down in a controlled - manner. The archetypical example of using ``with`` is when processing files. - If only a single expression is supplied, or the argument is `_`, then no - variable is bound to the expression, as shown below. + ``with`` compiles to a :py:keyword:`with` statement, which wraps some code + with one or more :ref:`context managers `. The first + argument is a bracketed list of context managers, and the remaining + arguments are body forms. - Examples: - :: + The manager list can't be empty. If it has only one item, that item is + evaluated to obtain the context manager to use. If it has two, the first + argument (a symbol) is bound to the result of the second. Thus, ``(with + [(f)] …)`` compiles to ``with f(): …`` and ``(with [x (f)] …)`` compiles to + ``with f() as x: …``. :: - => (with [arg (expr)] block) - => (with [(expr)] block) - => (with [arg1 (expr1) _ (expr2) arg3 (expr3)] block) + (with [o (open "file.txt" "rt")] + (print (.read o))) - The following example will open the ``NEWS`` file and print its content to the - screen. The file is automatically closed after it has been processed:: + If the manager list has more than two items, they're understood as + variable-manager pairs; thus :: - => (with [f (open \"NEWS\")] (print (.read f))) + (with [v1 e1 v2 e2 v3 e3] …) - ``with`` returns the value of its last form, unless it suppresses an exception - (because the context manager's ``__exit__`` method returned true), in which - case it returns ``None``. So, the previous example could also be written:: + compiles to - => (print (with [f (open \"NEWS\")] (.read f))) + .. code-block:: python -.. hy:function:: (with/a [#* args]) + with e1 as v1, e2 as v2, e3 as v3: … - Wrap execution of `body` within a context manager given as bracket `args`. - ``with/a`` behaves like ``with``, but is used to wrap the execution of a block - within an asynchronous context manager. The context manager can then set up - the local system and tear it down in a controlled manner asynchronously. - Examples: + The symbol ``_`` is interpreted specially as a variable name in the manager + list: instead of binding the context manager to the variable ``_`` (as + Python's ``with e1 as _: …``), ``with`` will leave it anonymous (as Python's + ``with e1: …``). - :: - => (with/a [arg (expr)] block) - => (with/a [(expr)] block) - => (with/a [_ (expr) arg (expr) _ (expr)] block) + ``with`` returns the value of its last form, unless it suppresses an + exception (because the context manager's ``__exit__`` method returned true), + in which case it returns ``None``. So, the previous example could also be + written :: - .. note:: - ``with/a`` returns the value of its last form, unless it suppresses an exception - (because the context manager's ``__aexit__`` method returned true), in which - case it returns ``None``. + (print (with [o (open "file.txt" "rt")] (.read o))) -.. hy:function:: (yield [object]) +.. hy:macro:: (with/a [managers #* body]) - ``yield`` is used to create a generator object that returns one or more values. - The generator is iterable and therefore can be used in loops, list - comprehensions and other similar constructs. + As :hy:func:`with`, but compiles to an :py:keyword:`async with` statement. - The function ``random-numbers`` shows how generators can be used to generate - infinite series without consuming infinite amount of memory. +.. hy:macro:: (yield [value]) - :strong:`Examples` + ``yield`` compiles to a :ref:`yield expression `, which + returns a value as a generator. As in Python, one argument, the value to + yield, is accepted, and it defaults to ``None``. :: - :: + (defn naysayer [] + (while True + (yield "nope"))) + (hy.repr (list (zip "abc" (naysayer)))) + ; => [#("a" "nope") #("b" "nope") #("c" "nope")] + + For ``yield from``, see :hy:func:`yield-from`. - => (defn multiply [bases coefficients] - ... (for [#(base coefficient) (zip bases coefficients)] - ... (yield (* base coefficient)))) +.. hy:macro:: (yield-from [object]) - => (multiply (range 5) (range 5)) - + ``yield-from`` compiles to a :ref:`yield-from expression `, + which returns a value from a subgenerator. The syntax is the same as that of + :hy:func:`yield`. :: - => (list (multiply (range 10) (range 10))) - [0 1 4 9 16 25 36 49 64 81] + (defn myrange [] + (setv r (range 10)) + (while True + (yield-from r))) + (hy.repr (list (zip "abc" (myrange)))) + ; => [#("a" 0) #("b" 1) #("c" 2)] - => (import random) - => (defn random-numbers [low high] - ... (while True (yield (.randint random low high)))) - => (list (take 15 (random-numbers 1 50))) - [7 41 6 22 32 17 5 38 18 38 17 14 23 23 19] +.. hy:macro:: (deftype [args]) + ``deftype`` compiles to a :py3_12:keyword:`type` statement, which defines a + type alias. It requires Python 3.12. Its arguments optionally begin with + ``:tp`` and a list of type parameters (as in :hy:func:`defn`), then specify + the name for the new alias and its value. :: -.. hy:function:: (yield-from [object]) + (deftype IntOrStr (| int str)) + (deftype :tp [T] ListOrSet (| (get list T) (get set T))) - .. versionadded:: 0.9.13 +.. hy:macro:: (pragma [#* args]) - ``yield-from`` is used to call a subgenerator. This is useful if you - want your coroutine to be able to delegate its processes to another - coroutine, say, if using something fancy like - `asyncio `_. + ``pragma`` is used to adjust the state of the compiler. It's called for its + side-effects, and returns ``None``. The arguments are key-value pairs, like a + function call with keyword arguments:: + + (pragma :prag1 value1 :prag2 (get-value2)) + + Each key is a literal keyword giving the name of a pragma. Each value is an + arbitrary form, which is evaluated as ordinary Hy code but at compile-time. + + The effect of each pragma is locally scoped to its containing function, + class, or comprehension form (other than ``for``), if there is one. + + Only one pragma is currently implemented: + +.. _warn-on-core-shadow: + + - ``:warn-on-core-shadow``: If true (the default), :hy:func:`defmacro` and + :hy:func:`require` will raise a warning at compile-time if you define a macro + with the same name as a core macro. Shadowing a core macro in this fashion is + dangerous, because other macros may call your new macro when they meant to + refer to the core macro. .. hy:automodule:: hy.core.macros - :members: :macros: - :tags: Placeholder macros ~~~~~~~~~~~~~~~~~~ @@ -1437,6 +1328,7 @@ expanded, is crash, regardless of their arguments: - ``else`` - ``except`` +- ``except*`` - ``finally`` - ``unpack-mapping`` - ``unquote`` @@ -1480,23 +1372,11 @@ the following methods .. hy:autofunction:: hy.as-model -.. hy:data:: hy.errors.COLORED - - This variable is initially :py:data:`False`. If it's set to a true value, Hy - will color its error messages with ``colorama``. - -.. _reader-macros: +.. hy:autoclass:: hy.I Reader Macros ------------- -Like regular macros, reader macros should return a Hy form that will then be -passed to the compiler for execution. Reader macros access the Hy reader using -the ``&reader`` name. It gives access to all of the text- and form-parsing logic -that Hy uses to parse itself. See :py:class:`HyReader ` and -its base class :py:class:`Reader ` for details regarding -the available processing methods. - .. autoclass:: hy.reader.hy_reader.HyReader :members: parse, parse_one_form, parse_forms_until, read_default, fill_pos @@ -1508,9 +1388,3 @@ Python Operators .. hy:automodule:: hy.pyops :members: - -Reserved --------- - -.. hy:automodule:: hy.reserved - :members: diff --git a/docs/cli.rst b/docs/cli.rst index e1e34cfb3..10bc1756b 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,62 +1,43 @@ ====================== -Command Line Interface +Command-Line Interface ====================== -.. _hy: +Hy provides a handful of command-line programs for working with Hy code. + +.. _hy-cli: hy -- -Command Line Options -^^^^^^^^^^^^^^^^^^^^ - -.. cmdoption:: -c - - Execute the Hy code in *command*. - - .. code-block:: bash - - $ hy -c "(print (+ 2 2))" - 4 - -.. cmdoption:: -i - - Execute the Hy code in *command*, then stay in REPL. +``hy`` is a command-line interface for Hy that in general imitates the program +``python`` provided by CPython. For example, ``hy`` without arguments launches +the :ref:`REPL ` if standard input is a TTY and runs the standard input +as a script otherwise, whereas ``hy foo.hy a b`` runs the Hy program +``foo.hy`` with ``a`` and ``b`` as command-line arguments. See ``hy --help`` +for a complete list of options and :py:ref:`Python's documentation +` for many details. Here are some Hy-specific details: .. cmdoption:: -m - Execute the Hy code in *module*, including ``defmain`` if defined. - - The :option:`-m` flag terminates the options list so that - all arguments after the *module* name are passed to the module in - ``sys.argv``. - - .. versionadded:: 0.11.0 + Much like Python's ``-m``, but the input module name will be :ref:`mangled + `. .. cmdoption:: --spy - Print equivalent Python code before executing in REPL. For example:: - - => (defn salutationsnm [name] (print (+ "Hy " name "!"))) - def salutationsnm(name): - return print('Hy ' + name + '!') - => (salutationsnm "YourName") - salutationsnm('YourName') - Hy YourName! - => + Print equivalent Python code before executing each piece of Hy code in the + REPL:: - `--spy` only works on REPL mode. - .. versionadded:: 0.9.11 + => (+ 1 2) + 1 + 2 + ------------------------------ + 3 .. cmdoption:: --repl-output-fn - Format REPL output using specific function (e.g., ``repr``) - - .. versionadded:: 0.13.0 - -.. cmdoption:: -v - - Print the Hy version number and exit. + Set the :ref:`REPL output function `. This can be the + name of a Python builtin, most likely ``repr``, or a dotted name like + ``foo.bar.baz``. In the latter case, Hy will attempt to import the named + object with code like ``(import foo.bar [baz])``. .. _hy2py: @@ -64,10 +45,10 @@ Command Line Options hy2py ----- -``hy2py`` is a program to convert Hy source code into Python source code. Use ``hy2py --help`` for usage instructions. It can take its input from standard input or from a filename provided as a command-line argument. The result is written to standard output. +``hy2py`` is a program to convert Hy source code into Python source code. Use ``hy2py --help`` for usage instructions. It can take its input from standard input, or from a file or module name provided as a command-line argument. In the case of a module name, the current working directory should be the parent directory of that module, and the output parameter (``--output/-o``) is required. When the output parameter is provided, the output will be written into the given folder or file. Otherwise, the result is written to standard output. .. warning:: - ``hy2py`` can execute arbitrary code. Don't give it untrusted input. + ``hy2py`` can execute arbitrary code (via macros, :hy:func:`eval-when-compile`, etc.). Don't give it untrusted input. @@ -79,4 +60,4 @@ hyc ``hyc`` is a program to compile files of Hy code into Python bytecode. Use ``hyc --help`` for usage instructions. The generated bytecode files are named and placed according to the usual scheme of your Python executable, as indicated by :py:func:`importlib.util.cache_from_source`. .. warning:: - ``hyc`` can execute arbitrary code. Don't give it untrusted input. + ``hyc`` can execute arbitrary code (via macros, :hy:func:`eval-when-compile`, etc.). Don't give it untrusted input. diff --git a/docs/conf.py b/docs/conf.py index 8c00214b3..e12278d77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,8 +68,13 @@ intersphinx_mapping = dict( py=("https://docs.python.org/3/", None), py3_10=("https://docs.python.org/3.10/", None), + py3_12=("https://docs.python.org/3.12/", None), hyrule=("https://hyrule.readthedocs.io/en/master/", None), ) + +import hy +hy.I = type(hy.I) # A trick to enable `hy:autoclass:: hy.I` + # ** Generate Cheatsheet import json from itertools import zip_longest diff --git a/docs/coreteam.rst b/docs/coreteam.rst index cd9a48ecb..7b04dca87 100644 --- a/docs/coreteam.rst +++ b/docs/coreteam.rst @@ -1,10 +1,10 @@ +* `Peter Andreev `_ * `Kodi B. Arfer `_ -* `Nicolas Dandrimont `_ +* `Allie Jo Casey `_ +* `Sunjay Cauligi `_ * `Julien Danjou `_ -* `Rob Day `_ * `Simon Gomizelj `_ * `Ryan Gonzalez `_ -* `Abhishek Lekshmanan `_ * `Morten Linderud `_ * `Matthew Odendahl `_ * `Paul Tagliamonte `_ diff --git a/docs/env_var.rst b/docs/env_var.rst index aca679426..202911153 100644 --- a/docs/env_var.rst +++ b/docs/env_var.rst @@ -11,24 +11,9 @@ set to anything else. (Default: nothing) Path to a file containing Hy source code to execute when starting the REPL. -.. envvar:: HY_COLORED_AST_OBJECTS +.. envvar:: HY_SHOW_INTERNAL_ERRORS - (Default: false) Whether to use ANSI color when printing the Python - :func:`repr`\s of Hy :ref:`models `. - -.. envvar:: HY_COLORED_ERRORS - - (Default: false) Whether to use ANSI color when printing certain error - messages. - -.. envvar:: HY_DEBUG - - (Default: false) Does something mysterious that's probably similar to - ``HY_FILTER_INTERNAL_ERRORS``. - -.. envvar:: HY_FILTER_INTERNAL_ERRORS - - (Default: true) Whether to hide some parts of tracebacks that point to + (Default: false) Whether to show some parts of tracebacks that point to internal Hy code and won't be helpful to the typical Hy user. .. envvar:: HY_HISTORY diff --git a/docs/hacking.rst b/docs/hacking.rst index 96e2cc85c..90d0a37d9 100644 --- a/docs/hacking.rst +++ b/docs/hacking.rst @@ -1,112 +1,14 @@ .. _hacking: =============== - Hacking on Hy + Developing Hy =============== -.. highlight:: bash - -Join our Hyve! -============== - -Please come hack on Hy! - -Please come hang out with us on `the Github Discussions page `_! - -Please talk about it on Twitter with the ``#hy`` hashtag! - -Please blog about it! - -Please don't spraypaint it on your neighbor's fence (without asking nicely)! - - -Hack! -===== - -Do this: - -1. Create a `virtual environment - `_:: - - $ virtualenv venv - - and activate it:: - - $ . venv/bin/activate - - or use `virtualenvwrapper `_ - to create and manage your virtual environment:: - - $ mkvirtualenv hy - $ workon hy - -2. Get the source code:: - - $ git clone https://github.com/hylang/hy.git - - or use your fork:: - - $ git clone git@github.com:/hy.git - -3. Install for hacking:: - - $ cd hy/ - $ pip install -e . - -4. Install other develop-y requirements:: - - $ pip install -r requirements-dev.txt - -5. Optionally, enable the `pre-commit `_ hooks defined in ``.pre-commit-config.yaml``:: - - $ pre-commit install - - This will ensure your code adheres to the formatting conventions enforced via continuous integration (CI). - -6. Optionally, tell ``git blame`` to ignore the commits listed in ``.git-blame-ignore-revs``:: - - $ git config blame.ignoreRevsFile .git-blame-ignore-revs - - This file is intended to contains commits with large diffs but negligible semantic changes. - -7. Do awesome things; make someone shriek in delight/disgust at what - you have wrought. - - -Test! -===== - -Tests are located in ``tests/``. We use `pytest `_. - -To run the tests:: - - $ pytest - -Write tests---tests are good! - -Also, it is good to run the tests for all the platforms supported and for -PEP 8 compliant code. You can do so by running tox:: - - $ tox - -Document! -========= - -Documentation is located in ``docs/``. We use `Sphinx -`_. - -To build the docs in HTML:: - - $ cd docs - $ sphinx-build . _build -b html - -Write docs---docs are good! Even this doc! - .. include:: ../CONTRIBUTING.rst Core Team ========= -The core development team of Hy consists of following developers: +Hy's core development team consists of the following people: .. include:: coreteam.rst diff --git a/docs/index.rst b/docs/index.rst index 2b77e6199..b6b855378 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,10 +14,16 @@ Hy is a Lisp dialect that's embedded in Python. Since Hy transforms its Lisp code into Python abstract syntax tree (AST) objects, you have the whole beautiful world of Python at your fingertips, in Lisp form. +.. Changes to the below paragraph should be mirrored on Hy's homepage + and the README. + To install the latest release of Hy, just use the command ``pip3 install --user hy``. Then you can start an interactive read-eval-print loop (REPL) with the command ``hy``, or run a Hy program with ``hy myprogram.hy``. +Hy is tested on all released and currently maintained versions of CPython (on +Linux and Windows), and on recent versions of PyPy and Pyodide. + .. toctree:: :maxdepth: 3 diff --git a/docs/interop.rst b/docs/interop.rst index 4df99f4bd..8c3b813f7 100644 --- a/docs/interop.rst +++ b/docs/interop.rst @@ -1,129 +1,67 @@ .. _interop: -===================== -Hy <-> Python interop -===================== - -Despite being a Lisp, Hy aims to be fully compatible with Python. That means -every Python module or package can be imported in Hy code, and vice versa. +======================= +Python Interoperability +======================= :ref:`Mangling ` allows variable names to be spelled differently in Hy and Python. For example, Python's ``str.format_map`` can be written ``str.format-map`` in Hy, and a Hy function named ``valid?`` would be called -``is_valid`` in Python. In Python, you can import Hy's core functions -``mangle`` and ``unmangle`` directly from the ``hy`` package. +``hyx_valid_Xquestion_markX`` in Python. You can call :hy:func:`hy.mangle` and +:hy:func:`hy.unmangle` from either language. Using Python from Hy ==================== -You can embed Python code directly into a Hy program with the special operators -:hy:func:`py ` and :hy:func:`pys `. - -Using a Python module from Hy is nice and easy: you just have to :ref:`import` -it. If you have the following in ``greetings.py`` in Python: - -.. code-block:: python - - def greet(name): - print("hello," name) - -You can use it in Hy:: +To use a Python module from Hy, just :hy:func:`import` it. In most cases, no +additional ceremony is required. - (import greetings) - (.greet greetings "foo") ; prints "hello, foo" - -You can also import ``.pyc`` bytecode files, of course. +You can embed Python code directly into a Hy program with the macros +:hy:func:`py ` and :hy:func:`pys `, and you can use standard Python +tools like :func:`eval` or :func:`exec` to execute or manipulate Python code in +strings. Using Hy from Python ==================== -Suppose you have written some useful utilities in Hy, and you want to use them in -regular Python, or to share them with others as a package. Or suppose you work -with somebody else, who doesn't like Hy (!), and only uses Python. - -In any case, you need to know how to use Hy from Python. Fear not, for it is -easy. +To use a Hy module from Python, you can just :py:keyword:`import` it, provided +that ``hy`` has already been imported first, whether in the current module or +in some earlier module executed by the current Python process. The ``hy`` +import is necessary to create the hooks that allow importing Hy modules. Note +that you can always have a wrapper Python file (such as a package's +``__init__.py``) do the ``import hy`` for the user; this is a smart thing to do +for a published package. -If you save the following in ``greetings.hy``:: +No way to import macros or reader macros into a Python module is implemented, +since there's no way to call them in Python anyway. - (setv this-will-have-underscores "See?") - (defn greet [name] (print "Hello from Hy," name)) +You can use :ref:`hy2py` to convert a Hy program to Python. The output will +still import ``hy``, and thus require Hy to be installed in order to run; see +:ref:`implicit-names` for details and workarounds. -Then you can use it directly from Python, by importing Hy before importing -the module. In Python: +To execute Hy code from a string, use :hy:func:`hy.read-many` to convert it to +:ref:`models ` and :hy:func:`hy.eval` to evaluate it: .. code-block:: python - import hy - import greetings - - greetings.greet("Foo") # prints "Hello from Hy, Foo" - print(greetings.this_will_have_underscores) # prints "See?" - -If you create a package with Hy code, and you do the ``import hy`` in -``__init__.py``, you can then directly include the package. Of course, Hy still -has to be installed. - -Compiled files --------------- - -You can also compile a module with ``hyc``, which gives you a ``.pyc`` file. You -can import that file. Hy does not *really* need to be installed ; however, if in -your code, you use any symbol from :doc:`/api`, a corresponding ``import`` -statement will be generated, and Hy will have to be installed. - -Even if you do not use a Hy builtin, but just another function or variable with -the name of a Hy builtin, the ``import`` will be generated. For example, the previous code -causes the import of ``name`` from ``hy.core.language``. - -**Bottom line: in most cases, Hy has to be installed.** - -.. _repl-from-py: + >>> hy.eval(hy.read_many("(setv x 1) (+ x 1)")) + 2 -Launching a Hy REPL from Python -------------------------------- +There is no Hy equivalent of :func:`exec` because :hy:func:`hy.eval` works +even when the input isn't equivalent to a single Python expression. -You can use the function ``run_repl()`` to launch the Hy REPL from Python: +You can use :meth:`hy.REPL.run` to launch the Hy REPL from Python, as in +``hy.REPL(locals = locals()).run()``. -.. code-block:: python - - >>> import hy.cmdline - >>> hy.cmdline.run_repl() - hy 0.12.1 using CPython(default) 3.6.0 on Linux - => (defn foo [] (print "bar")) - => (test) - bar - -If you want to print the Python code Hy generates for you, use the ``spy`` -argument: - -.. code-block:: python - - >>> import hy.cmdline - >>> hy.cmdline.run_repl(spy=True) - hy 0.12.1 using CPython(default) 3.6.0 on Linux - => (defn test [] (print "bar")) - def test(): - return print('bar') - => (test) - 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`` function to turn the expression -into a Hy model: - -.. code-block:: python - - >>> import hy - >>> expr = hy.read("(- (/ (+ 1 3 88) 2) 8)") - -Then, use the ``hy.eval`` function to evaluate it: - -.. code-block:: python +Libraries that expect Python +============================ - >>> hy.eval(expr) - 38.0 +There are various means by which Hy may interact poorly with a Python library because the library doesn't account for the possibility of Hy. For example, +when you run :ref:`hy-cli`, ``sys.executable`` will be set to +this program rather than the original Python binary. This is helpful more often +than not, but will lead to trouble if e.g. the library tries to call +:py:data:`sys.executable` with the ``-c`` option. In this case, you can try +setting :py:data:`sys.executable` back to ``hy.sys-executable``, which is a +saved copy of the original value. More generally, you can use :ref:`hy2py`, or you +can put a simple Python wrapper script like ``import hy, my_hy_program`` in +front of your code. diff --git a/docs/macros.rst b/docs/macros.rst index 73e160aef..c4ad7e40f 100644 --- a/docs/macros.rst +++ b/docs/macros.rst @@ -2,77 +2,221 @@ Macros ====== -.. _using-gensym: +Macros, and the metaprogramming they enable, are one of the characteristic features of Lisp, and one of the main advantages Hy offers over vanilla Python. Much of the material covered in this chapter will be familiar to veterans of other Lisps, but there are also a lot of Hyly specific details. -Using gensym for Safer Macros ------------------------------ +What are macros for? +-------------------- -When writing macros, one must be careful to avoid capturing external variables -or using variable names that might conflict with user code. +The gist of `metaprogramming +`_ is that it allows you to program the programming language itself (hence the word). You can create new control structures, like :ref:`do-while `, or other kinds of new syntax, like a concise literal notation for your favorite data structure. You can also modify how existing syntax is understood within a region of code, as by making identifiers that start with a capital letter implicitly imported from a certain module. Finally, metaprogramming can improve performance in some cases by effectively inlining functions, or by computing something once at compile-time rather than several times at run-time. With a Lisp-like macro system, you can metaprogram in a slicker and less error-prone way than generating code as text with conventional string formatting, or with lexer-level macros like those provided by the C preprocessor. -We will use an example macro ``nif`` (see http://letoverlambda.com/index.cl/guest/chap3.html#sec_5 -for a more complete description.) ``nif`` is an example, something like a numeric ``if``, -where based on the expression, one of the 3 forms is called depending on if the -expression is positive, zero or negative. +Types of macros +--------------- -A first pass might be something like:: +Hy offers two types of macros: regular macros and reader macros. - (defmacro nif [expr pos-form zero-form neg-form] - `(do - (setv obscure-name ~expr) - (cond (> obscure-name 0) ~pos-form - (= obscure-name 0) ~zero-form - (< obscure-name 0) ~neg-form))) +**Regular macros**, typically defined with :hy:func:`defmacro`, are the kind Lispers usually mean when they talk about "macros". Regular macros are called like a function, with an :ref:`expression ` whose head is the macro name: for example, ``(foo a b)`` could call a macro named ``foo``. A regular macro is called at compile-time, after the entire top-level form in which it appears is parsed, and receives parsed :ref:`models ` as arguments. Regular macros come in :ref:`three varieties, which vary in scope `. + +**Reader macros**, typically defined with :hy:func:`defreader`, are lower-level than regular macros. They're called with the hash sign ``#``; for example, ``#foo`` calls a reader macro named ``foo``. A reader macro is called at parse-time. It doesn't receive conventional arguments. Instead, it uses an implicitly available parser object to parse the subsequent source text. When it returns, the standard Hy parser picks up where it left off. + +Related constructs +~~~~~~~~~~~~~~~~~~ + +There are three other constructs that perform compile-time processing much like macros, and hence are worth mentioning here. + +- :hy:func:`do-mac` is essentially shorthand for defining and then immediately calling a regular macro with no arguments. +- :hy:func:`eval-when-compile` evaluates some code at compile-time, but contributes no code to the final program, like a macro that returns ``None`` in a context where the ``None`` doesn't do anything. +- :hy:func:`eval-and-compile` evaluates some code at compile-time, like :hy:func:`eval-when-compile`, but also leaves the same code to be re-evaluated at run-time. + +When to use what +~~~~~~~~~~~~~~~~ + +The variety of options can be intimidating. In addition to all of Hy's features listed above, Python is a dynamic programming language that allows you to do a lot of things at run-time that other languages would blanch at. For example, you can dynamically define a new class by calling :class:`type`. So, watch out for cases where your first thought is to use a macro, but you don't actually need one. + +When deciding what to use, a good rule of thumb is to use the least powerful option that suffices for the syntax, semantics, and performance that you want. So first, see if Python's dynamic features are enough. If they aren't, try a macro-like construct or a regular macro. If even those aren't enough, try a reader macro. Using the least powerful applicable option will help you avoid the :ref:`macro pitfalls described below `, as well as other headaches such as wanting to use a macro where a Python API needs a function. (For the sake of providing simpler examples, much of the below discussion will ignore this advice and consider macros that could easily be written as functions.) + +The basics +---------- + +A regular macro can be defined with :hy:func:`defmacro` using a syntax similar to that of :hy:func:`defn`. Here's how you could define and call a trivial macro that takes no arguments and returns a constant:: + + (defmacro seventeen [] + 17) + + (print (seventeen)) + +To see that ``seventeen`` is expanded at compile-time, run ``hy2py`` on this script and notice that it ends with ``print(17)`` rather than ``print(seventeen())``. If you insert a ``print`` call inside the macro definition, you'll also see that the print happens when the file is compiled, but not when it's rerun (provided an up-to-date bytecode file exists). + +A more useful macro returns code. You can construct a model the long way, like this:: + + (defmacro addition [] + (hy.models.Expression [ + (hy.models.Symbol "+") + (hy.models.Integer 1) + (hy.models.Integer 1)])) + +or more concisely with :hy:func:`quote`, like this:: + + (defmacro addition [] + '(+ 1 1)) + +You don't need to always return a model because the compiler calls :hy:func:`hy.as-model` on everything before trying to compile it. Thus, the ``17`` above works fine in place of ``(hy.models.Integer 17)``. But trying to compile something that ``hy.as-model`` chokes on, like a function object, is an error. + +Arguments are always passed in as models. You can use quasiquotation (see :hy:func:`quasiquote`) to concisely define a model with partly literal and partly evaluated components:: + + (defmacro set-to-2 [variable] + `(setv ~variable 2)) + (set-to-2 foobar) + (print foobar) + +Macros don't understand keyword arguments like functions do. Rather, the :ref:`keyword objects ` themselves are passed in literally. This gives you flexibility in how to handle them. Thus, ``#** kwargs`` and ``*`` aren't allowed in the parameter list of a macro, although ``#* args`` and ``/`` are. + +On the inside, macros are functions, and obey the usual Python semantics for functions. For example, :hy:func:`setv` inside a macro will define or modify a variable local to the current macro call, and :hy:func:`return` ends macro execution and uses its argument as the expansion. + +Macros from other modules can be brought into the current scope with :hy:func:`require`. + +.. _macro-pitfalls: -where ``obscure-name`` is an attempt to pick some variable name as not to -conflict with other code. But of course, while well-intentioned, -this is no guarantee. +Pitfalls +-------- -The method :hy:func:`gensym ` is designed to generate a new, unique symbol for just -such an occasion. A much better version of ``nif`` would be:: +Macros are powerful, but with great power comes great potential for anguish. There are a few characteristic issues you need to guard against to write macros well, and, to a lesser extent, even to use macros well. - (defmacro nif [expr pos-form zero-form neg-form] +Name games +~~~~~~~~~~ + +A lot of these issues are variations on the theme of names not referring to what you intend them to, or in other words, surprise shadowing. For example, the macro below was intended to define a new variable named ``x``, but it ends up modifying a preexisting variable. :: + + (defmacro upper-twice [arg] + `(do + (setv x (.upper ~arg)) + (+ x x))) + (setv x "Okay guys, ") + (setv salutation (upper-twice "bye")) + (print (+ x salutation)) + ; Intended result: "Okay guys, BYEBYE" + ; Actual result: "BYEBYEBYE" + +If you avoid the assignment entirely, by using an argument more than once, you can cause a different problem: surprise multiple evaluation. :: + + (defmacro upper-twice [arg] + `(+ (.upper ~arg) (.upper ~arg))) + (setv items ["a" "b" "c"]) + (print (upper-twice (.pop items))) + ; Intended result: "CC" + ; Actual result: "CB" + +A better approach is to use :hy:func:`hy.gensym` to choose your variable name:: + + (defmacro upper-twice [arg] (setv g (hy.gensym)) `(do - (setv ~g ~expr) - (cond (> ~g 0) ~pos-form - (= ~g 0) ~zero-form - (< ~g 0) ~neg-form))) + (setv ~g (.upper ~arg)) + (+ ~g ~g))) + +Hyrule provides some macros that make using gensyms more convenient, like :hy:func:`defmacro! ` and :hy:func:`with-gensyms `. + +Macro subroutines +~~~~~~~~~~~~~~~~~ + +A case where you could want something to be in the scope of a macro's expansion, and then it turns out not to be, is when you want to call a function or another macro in the expansion:: + + (defmacro hypotenuse [a b] + (import math) + `(math.sqrt (+ (** ~a 2) (** ~b 2)))) + (print (hypotenuse 3 4)) + ; NameError: name 'math' is not defined + +The form ``(import math)`` here appears in the wrong context, in the macro call itself rather than the expansion. You could use ``import`` or ``require`` to bind the module name or one of its members to a gensym, but an often more convenient option is to use the one-shot import syntax :hy:class:`hy.I` or the one-shot require syntax :ref:`hy.R `:: + + (defmacro hypotenuse [a b] + `(hy.I.math.sqrt (+ (** ~a 2) (** ~b 2)))) + (hypotenuse 3 4) + +A related but distinct issue is when you want to use a function (or other ordinary Python object) in a macro's code, but it isn't available soon enough:: + + (defn subroutine [x] + (hy.models.Symbol (.upper x))) + (defmacro uppercase-symbol [x] + (subroutine x)) + (setv (uppercase-symbol foo) 1) + ; NameError: name 'subroutine' is not defined + +Here, ``subroutine`` is only defined at run-time, so ``uppercase-symbol`` can't see it when it's expanding (unless you happen to be calling ``uppercase-symbol`` from a different module). This is easily worked around by wrapping ``(defn subroutine …)`` in :hy:func:`eval-and-compile` (or :hy:func:`eval-when-compile` if you want ``subroutine`` to be invisible at run-time). -This is an easy case, since there is only one symbol. But if there is -a need for several gensym's there is a second macro :hy:func:`with-gensyms ` that -basically expands to a ``setv`` form:: +By the way, despite the need for ``eval-and-compile``, extracting a lot of complex logic out of a macro into a function is often a good idea. Functions are typically easier to debug and to make use of in other macros. - (with-gensyms [a b c] - ...) +The important take-home big fat WARNING +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -expands to:: +Ultimately it's wisest to use only four kinds of names in macro expansions: gensyms, core macros, objects that Python puts in scope by default (like its built-in functions), and ``hy`` and its attributes. It's possible to rebind nearly all these names, so surprise shadowing is still theoretically possible. Unfortunately, the only way to prevent these pathological rebindings from coming about is… don't do that. Don't make a new macro named ``setv`` or name a function argument ``type`` unless you're ready for every macro you call to break, the same way you wouldn't monkey-patch a built-in Python module without thinking carefully. This kind of thing is the responsibility of the macro caller; the macro writer can't do much to defend against it. There is at least a pragma :ref:`warn-on-core-shadow `, enabled by default, that causes ``defmacro`` and ``require`` to warn you if you give your new macro the same name as a core macro. + +.. _reader-macros: + +Reader macros +------------- + +Reader macros allow you to hook directly into Hy's parser to customize how text is parsed into models. They're defined with :hy:func:`defreader`, or, like regular macros, brought in from other modules with :hy:func:`require`. Rather than receiving function arguments, a reader macro has access to a :py:class:`HyReader ` object named ``&reader``, which provides all the text-parsing logic that Hy uses to parse itself (see :py:class:`HyReader ` and its base class :py:class:`Reader ` for the available methods). A reader macro is called with the hash sign ``#``, and like a regular macro, it should return a model or something convertible to a model. + +Here's a moderately complex example of a reader macro that couldn't be implemented as a regular macro. It reads in a list of lists in which the inner lists are newline-separated, but newlines are allowed inside elements. :: + + (defreader matrix + (.slurp-space &reader) + (setv start (.getc &reader)) + (assert (= start "[")) + (.slurp-space &reader) + (setv out [[]]) + (while (not (.peek-and-getc &reader "]")) + (cond + (any (gfor c " \t" (.peek-and-getc &reader c))) + None + (.peek-and-getc &reader "\n") + (.append out []) + True + (.append (get out -1) (.parse-one-form &reader)))) + (lfor line out :if line line)) + + (print (hy.repr #matrix [ + 1 (+ 1 1) 3 + 4 ["element" "containing" + "a" "newline"] 6 + 7 8 9])) + ; => [[1 2 3] [4 ["element" "containing" "a" "newline"] 6] [7 8 9]] + +Note that because reader macros are evaluated at parse-time, and top-level forms are completely parsed before any further compile-time execution occurs, you can't use a reader macro in the same top-level form that defines it:: (do - (setv a (hy.gensym) - b (hy.gensym) - c (hy.gensym)) - ...) + (defreader up + (.slurp-space &reader) + (.upper (.read-one-form &reader))) + (print #up "hello?")) + ; LexException: reader macro '#up' is not defined -so our re-written ``nif`` would look like:: +.. _macro-namespaces: - (defmacro nif [expr pos-form zero-form neg-form] - (with-gensyms [g] - `(do - (setv ~g ~expr) - (cond (> ~g 0) ~pos-form - (= ~g 0) ~zero-form - (< ~g 0) ~neg-form)))) +Macro namespaces and operations on macros +----------------------------------------- -Finally, though we can make a new macro that does all this for us. :hy:func:`defmacro/g! ` -will take all symbols that begin with ``g!`` and automatically call ``gensym`` with the -remainder of the symbol. So ``g!a`` would become ``(hy.gensym "a")``. +Macros don't share namespaces with ordinary Python objects. That's why something like ``(defmacro m []) (print m)`` fails with a ``NameError``, and how :hy:mod:`hy.pyops` can provide a function named ``+`` without hiding the core macro ``+``. -Our final version of ``nif``, built with ``defmacro/g!`` becomes:: +There are three scoped varieties of regular macro. First are **core macros**, which are built into Hy; :ref:`the set of core macros ` is fixed. They're available by default. You can inspect them in the dictionary ``bulitins._hy_macros``, which is attached to Python's usual :py:mod:`builtins` module. The keys are strings giving :ref:`mangled ` names and the values are the function objects implementing the macros. - (defmacro/g! nif [expr pos-form zero-form neg-form] - `(do - (setv ~g!res ~expr) - (cond (> ~g!res 0) ~pos-form - (= ~g!res 0) ~zero-form - (< ~g!res 0) ~neg-form))) +**Global macros** are associated with modules, like Python global variables. They're defined when you call ``defmacro`` or ``require`` in a global scope. You can see them in the global variable ``_hy_macros`` associated with the same module. You can manipulate ``_hy_macros`` to list, add, delete, or get help on macros, but be sure to use :hy:func:`eval-and-compile` or :hy:func:`eval-when-compile` when you need the effect to happen at compile-time, which is often. (Modifying ``bulitins._hy_macros`` is of course a risky proposition.) Here's an example, which also demonstrates the core macro :hy:func:`get-macro `. ``get-macro`` provides syntactic sugar for getting all sorts of macros as objects. :: + + (defmacro m [] + "This is a docstring." + `(print "Hello, world.")) + (print (in "m" _hy_macros)) ; => True + (help (get-macro m)) + (m) ; => "Hello, world." + (eval-and-compile + (del (get _hy_macros "m"))) + (m) ; => NameError + (eval-and-compile + (setv (get _hy_macros (hy.mangle "new-mac")) (fn [] + '(print "Goodbye, world.")))) + (new-mac) ; => "Goodbye, world." + +**Local macros** are associated with function, class, or comprehension scopes, like Python local variables. They come about when you call ``defmacro`` or ``require`` in an appropriate scope. You can call :hy:func:`local-macros ` to view local macros, but adding or deleting elements is ineffective. + +Finally, ``_hy_reader_macros`` is a per-module dictionary like ``_hy_macros`` for reader macros, but here, the keys aren't mangled. There are no local reader macros, and there's no official way to introspect on Hy's handful of core reader macros. So, of the three scoped varieties of regular macro, reader macros most resemble global macros. diff --git a/docs/model_patterns.rst b/docs/model_patterns.rst index 44272bfa5..7759f7328 100644 --- a/docs/model_patterns.rst +++ b/docs/model_patterns.rst @@ -35,8 +35,9 @@ a pattern for a ``try`` form of the above kind: .. code-block:: clj - (import funcparserlib.parser [maybe many]) - (import hy.model-patterns [*]) + (import + funcparserlib.parser [maybe many] + hy.model-patterns *) (setv parser (whole [ (sym "try") (many (notpexpr "except" "else" "finally")) @@ -98,15 +99,16 @@ Here's how you could write a simple macro using model patterns: .. code-block:: clj (defmacro pairs [#* args] - (import funcparserlib.parser [many]) - (import hy.model-patterns [whole SYM FORM]) + (import + funcparserlib.parser [many] + hy.model-patterns [whole SYM FORM]) (setv [args] (.parse (whole [(many (+ SYM FORM))]) args)) `[~@(gfor [a1 a2] args #((str a1) a2))]) - (print (pairs a 1 b 2 c 3)) - ; => [["a" 1] ["b" 2] ["c" 3]] + (print (hy.repr (pairs a 1 b 2 c 3))) + ; => [#("a" 1) #("b" 2) #("c" 3)] A failed parse will raise ``funcparserlib.parser.NoParseError``. diff --git a/docs/repl.rst b/docs/repl.rst index 0f881b2cb..67af1540b 100644 --- a/docs/repl.rst +++ b/docs/repl.rst @@ -1,19 +1,21 @@ +.. _repl: + =========== The Hy REPL =========== Hy's `read-eval-print loop `_ (REPL) is implemented in -the class :class:`hy.cmdline.HyREPL`. The REPL can be started interactively +the class :class:`hy.REPL`. The REPL can be started interactively :doc:`from the command line ` or programmatically with the instance method -:meth:`hy.cmdline.HyREPL.run`. +:meth:`hy.REPL.run`. Two :doc:`environment variables ` useful for the REPL are ``HY_HISTORY``, which specifies where the REPL input history is saved, and ``HYSTARTUP``, which specifies :ref:`a file to run when the REPL starts `. -.. autoclass:: hy.cmdline.HyREPL +.. autoclass:: hy.REPL :members: run .. _repl-output-function: @@ -24,7 +26,7 @@ Output functions By default, the return value of each REPL input is printed with :hy:func:`hy.repr`. To change this, you can set the REPL output function with e.g. the command-line argument ``--repl-output-fn``. Use :func:`repr` to get -Python representations like Python's own REPL. +Python representations, like Python's own REPL. Regardless of the output function, no output is produced when the value is ``None``, as in Python. @@ -75,9 +77,9 @@ change the prompts. The following example shows a number of possibilities:: (setv repl-spy True repl-output-fn pformat - ;; We can even add colors to the prompts. - ;; This will set `=>` to green and `...` to red. + ;; Make the REPL prompt `=>` green. sys.ps1 "\x01\x1b[0;32m\x02=> \x01\x1b[0m\x02" + ;; Make the REPL prompt `...` red. sys.ps2 "\x01\x1b[0;31m\x02... \x01\x1b[0m\x02") (defn slurp [path] diff --git a/docs/semantics.rst b/docs/semantics.rst index 799395ccd..626c091d9 100644 --- a/docs/semantics.rst +++ b/docs/semantics.rst @@ -5,6 +5,8 @@ Semantics This chapter describes features of Hy semantics that differ from Python's and aren't better categorized elsewhere, such as in the chapter :doc:`macros`. +.. _implicit-names: + Implicit names -------------- @@ -47,10 +49,10 @@ called first, call it before ``f``. When bytecode is regenerated ---------------------------- -The first time Hy is asked to execute a file, it will produce a bytecode file +The first time Hy is asked to execute a file, whether directly or indirectly (as in the case of an import), it will produce a bytecode file (unless :std:envvar:`PYTHONDONTWRITEBYTECODE` is set). Subsequently, if the source file hasn't changed, Hy will load the bytecode instead of recompiling -the source. Python behaves similarly, but the difference between recompilation +the source. Python also makes bytecode files, but the difference between recompilation and loading bytecode is more consequential in Hy because of how Hy lets you run and generate code at compile-time with things like macros, reader macros, and :hy:func:`eval-and-compile`. You may be surprised by behavior like the diff --git a/docs/syntax.rst b/docs/syntax.rst index 5f4ba7867..9a3ab9a0d 100644 --- a/docs/syntax.rst +++ b/docs/syntax.rst @@ -26,12 +26,12 @@ somewhat different (such as :class:`Set `, which is ordered, unlike actual :class:`set`\s). All models inherit from :class:`Object `, which stores textual position information, so tracebacks can point to the right place in the code. The compiler takes whatever models -are left over after parsing and macro expansion and translates them into Python +are left over after parsing and macro-expansion and translates them into Python :mod:`ast` nodes (e.g., :class:`Integer ` becomes :class:`ast.Constant`), which can then be evaluated or rendered as Python code. Macros (that is, regular macros, as opposed to reader macros) operate on the model level, taking some models as arguments and returning more models for -compilation or further macro expansion; they're free to do quite different +compilation or further macro-expansion; they're free to do quite different things with a given model than the compiler does, if it pleases them to, like using an :class:`Integer ` to construct a :class:`Symbol `. @@ -59,9 +59,7 @@ The default representation of models (via :hy:func:`hy.repr`) uses quoting for readability, so ``(hy.models.Integer 5)`` is represented as ``'5``. Python representations (via :func:`repr`) use the constructors, and by default are pretty-printed; you can disable this globally by setting ``hy.models.PRETTY`` -to ``False``, or temporarily with the context manager ``hy.models.pretty``. You -can also color these Python representations with ``colorama`` by setting -``hy.models.COLORED`` to ``True``. +to ``False``, or temporarily with the context manager ``hy.models.pretty``. .. _hyobject: @@ -130,10 +128,11 @@ Identifiers Identifiers are a broad class of syntax in Hy, comprising not only variable names, but any nonempty sequence of characters that aren't ASCII whitespace nor -one of the following: ``()[]{};"'``. The reader will attempt to read each +one of the following: ``()[]{};"'`~``. The reader will attempt to read each identifier as a :ref:`numeric literal `, then attempt to read -it as a :ref:`keyword ` if that fails, then fall back on reading it -as a :ref:`symbol ` if that fails. +it as a :ref:`keyword ` if that fails, then attempt to read it as a +:ref:`dotted identifier ` if that fails, then fall back on +reading it as a :ref:`symbol ` if that fails. .. _numeric-literals: @@ -147,13 +146,18 @@ few extensions: - Commas (``,``) can be used like underscores (``_``) to separate digits without changing the result. Thus, ``10_000_000_000`` may also be written - ``10,000,000,000``. + ``10,000,000,000``. Hy is also more permissive about the placement of + separators than Python: several may be in a row, and they may be after all + digits, after ``.``, ``e``, or ``j``, or even inside a radix prefix. Separators + before the first digit are still forbidden because e.g. ``_1`` is a legal + Python variable name, so it's a symbol in Hy rather than an integer. - Integers can begin with leading zeroes, even without a radix prefix like ``0x``. Leading zeroes don't automatically cause the literal to be interpreted in octal like they do in C. For octal, use the prefix ``0o``, as in Python. - ``NaN``, ``Inf``, and ``-Inf`` are understood as literals. Each produces a - :class:`Float `. + :class:`Float `. These are case-sensitive, unlike other uses + of letters in numeric literals (``1E2``, ``0XFF``, ``5J``, etc.). - Hy allows complex literals as understood by the constructor for :class:`complex`, such as ``5+4j``. (This is also legal Python, but Hy reads it as a single :class:`Complex `, and doesn't otherwise @@ -178,8 +182,8 @@ Literal keywords are most often used for their special treatment in as values. For example, ``(f :foo 3)`` calls the function ``f`` with the parameter ``foo`` set to ``3``. The keyword is also :ref:`mangled ` at compile-time. To prevent a literal keyword from being treated specially in -an expression, you can :hy:func:`quote` the keyword, or you can use it itself -as a keyword argument, as in ``(f :foo :bar)``. +an expression, you can :hy:func:`quote` the keyword, or you can use it as the +value for another keyword argument, as in ``(f :foo :bar)``. Otherwise, keywords are simple model objects that evaluate to themselves. Users of other Lisps should note that it's often a better idea to use a string than a @@ -190,11 +194,41 @@ equivalent to ``{"a" 1 "b" 2}``, which is different from ``{:a 1 :b 2}`` (see :ref:`dict-literals`). The empty keyword ``:`` is syntactically legal, but you can't compile a -function call with an empty keyword argument. +function call with an empty keyword argument due to Python limitations. Thus +``(foo : 3)`` must be rewritten to use runtime unpacking, as in +``(foo #** {"" 3})``. .. autoclass:: hy.models.Keyword :members: __bool__, __call__ +.. _dotted-identifiers: + +Dotted identifiers +~~~~~~~~~~~~~~~~~~ + +Dotted identifiers are named for their use of the dot character ``.``, also +known as a period or full stop. They don't have their own model type because +they're actually syntactic sugar for :ref:`expressions `. Syntax +like ``foo.bar.baz`` is equivalent to ``(. foo bar baz)``. The general rule is +that a dotted identifier looks like two or more :ref:`symbols ` +(themselves not containing any dots) separated by single dots. The result is an +expression with the symbol ``.`` as its first element and the constituent +symbols as the remaining elements. + +A dotted identifier may also begin with one or more dots, as in ``.foo.bar`` or +``..foo.bar``, in which case the resulting expression has the appropriate head +(``.`` or ``..`` or whatever) and the symbol ``None`` as the following element. +Thus, ``..foo.bar`` is equivalent to ``(.. None foo bar)``. In the leading-dot +case, you may also use only one constitutent symbol. Thus, ``.foo`` is a legal +dotted identifier, and equivalent to ``(. None foo)``. + +See :ref:`the dot macro ` for what these expressions typically compile to. +See also the special behavior for :ref:`expressions ` that begin +with a dotted identifier that itself begins with a dot. Note that Hy provides +definitions of ``.`` and ``...`` by default, but not ``..``, ``....``, +``.....``, etc., so ``..foo.bar`` won't do anything useful by default outside +of macros that treat it specially, like :hy:func:`import`. + .. _symbols: Symbols @@ -208,7 +242,11 @@ the :class:`Symbol ` constructor (thus, :class:`Symbol Lisps). Some example symbols are ``hello``, ``+++``, ``3fiddy``, ``$40``, ``just✈wrong``, and ``🦑``. -As a special case, the symbol ``...`` compiles to the :class:`Ellipsis` object, +Dots are only allowed in a symbol if every character in the symbol is a dot. +Thus, ``a..b`` and ``a.`` are neither dotted identifiers nor symbols; they're +simply illegal syntax. + +As a special case, the symbol ``...`` compiles to the :data:`Ellipsis` object, as in Python. .. autoclass:: hy.models.Symbol @@ -235,10 +273,6 @@ Python-legal names. The steps are as follows: underscore into the name. Thus ``--has-dashes?`` becomes ``-_has_dashes?`` at this step. -#. If the name ends with ASCII ``?``, remove it and prepend ``is_``. Thus, - ``tasty?`` becomes ``is_tasty`` and ``-_has_dashes?`` becomes - ``is_-_has_dashes``. - #. If the name still isn't Python-legal, make the following changes. A name could be Python-illegal because it contains a character that's never legal in a Python name or it contains a character that's illegal in that position. @@ -251,12 +285,11 @@ Python-legal names. The steps are as follows: code point in lowercase hexadecimal. Thus, ``green☘`` becomes ``hyx_greenXshamrockX`` and - ``is_-_has_dashes`` becomes ``hyx_is_XhyphenHminusX_has_dashes``. + ``-_has_dashes`` becomes ``hyx_XhyphenHminusX_has_dashes``. #. Take any leading underscores removed in the first step, transliterate them - to ASCII, and add them back to the mangled name. Thus, ``(hy.mangle - '_tasty?)`` is ``"_is_tasty"`` instead of ``"is__tasty"`` and ``(hy.mangle - '__-_has-dashes?)`` is ``"__hyx_is_XhyphenHminusX_has_dashes"``. + to ASCII, and add them back to the mangled name. Thus, ``__green☘`` becomes + ``__hyx_greenXshamrockX``. #. Finally, normalize any leftover non-ASCII characters. The result may still not be ASCII (e.g., ``α`` is already Python-legal and normalized, so it @@ -269,11 +302,9 @@ You can invoke the mangler yourself with the function :hy:func:`hy.mangle`, and Mangling isn't something you should have to think about often, but you may see mangled names in error messages, the output of ``hy2py``, etc. A catch to be aware of is that mangling, as well as the inverse "unmangling" operation -offered by :hy:func:`hy.unmangle`, isn't one-to-one. Two different symbols -can mangle to the same string and hence compile to the same Python variable. -The chief practical consequence of this is that (non-initial) ``-`` and ``_`` are -interchangeable under mangling, so you can't use e.g. ``foo-bar`` and -``foo_bar`` as separate variables. +offered by :hy:func:`hy.unmangle`, isn't one-to-one. Two different symbols, +like ``foo-bar`` and ``foo_bar``, can mangle to the same string and hence +compile to the same Python variable. .. _string-literals: @@ -286,7 +317,7 @@ Hy allows double-quoted strings (e.g., ``"hello"``), but not single-quoted strings like Python. The single-quote character ``'`` is reserved for preventing the evaluation of a form, (e.g., ``'(+ 1 1)``), as in most Lisps (see :ref:`more-sugar`). Python's so-called triple-quoted strings (e.g., -``'''hello'''`` and ``"""hello"""``) aren't supported. However, in Hy, unlike +``'''hello'''`` and ``"""hello"""``) aren't supported, either. However, in Hy, unlike Python, any string literal can contain newlines; furthermore, Hy has :ref:`bracket strings `. For consistency with Python's triple-quoted strings, all literal newlines in literal strings are read as in @@ -298,8 +329,8 @@ Unrecognized escape sequences are a syntax error. To create a "raw string" that interprets all backslashes literally, prefix the string with ``r``, as in ``r"slash\not"``. -Like Python, Hy treats all string literals as sequences of Unicode characters -by default. The result is the model type :class:`String `. +By default, all string literals are regarded as sequences of Unicode characters. +The result is the model type :class:`String `. You may prefix a string literal with ``b`` to treat it as a sequence of bytes, producing :class:`Bytes ` instead. @@ -325,10 +356,10 @@ like the here-documents of other languages. A bracket string begins with begins with ``f-``, the bracket string is interpreted as an :ref:`f-string `.) For example:: - => (print #[["That's very kind of yuo [sic]" Tom wrote back.]]) - "That's very kind of yuo [sic]" Tom wrote back. - => (print #[==[1 + 1 = 2]==]) - 1 + 1 = 2 + (print #[["That's very kind of yuo [sic]" Tom wrote back.]]) + ; "That's very kind of yuo [sic]" Tom wrote back. + (print #[==[1 + 1 = 2]==]) + ; 1 + 1 = 2 Bracket strings are always raw Unicode strings, and don't allow the ``r`` or ``b`` prefixes. @@ -355,12 +386,35 @@ Expressions Expressions (:class:`Expression `) are denoted by parentheses: ``( … )``. The compiler evaluates expressions by checking the -first element. If it's a symbol, and the symbol is the name of a currently -defined macro, the macro is called. Otherwise, the expression is compiled into -a Python-level call, with the first element being the calling object. The -remaining forms are understood as arguments. Use :hy:func:`unpack-iterable` or -:hy:func:`unpack-mapping` to break up data structures into individual arguments -at runtime. +first element. + +- If it's a symbol, and the symbol is the name of a currently defined macro, + the macro is called. + + - Exception: if the symbol is also the name of a function in + :hy:mod:`hy.pyops`, and one of the arguments is an + :hy:func:`unpack-iterable` form, the ``pyops`` function is called instead + of the macro. This makes reasonable-looking expressions work that would + otherwise fail. For example, ``(+ #* summands)`` is understood as + ``(hy.pyops.+ #* summands)``, because Python provides no way to sum a list + of unknown length with a real addition expression. + +- If it is itself an expression of the form ``(. None …)`` (typically produced + with a :ref:`dotted identifier ` like ``.add``), it's used + 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 + remaining forms are understood as arguments. Use :hy:func:`unpack-iterable` + or :hy:func:`unpack-mapping` to break up data structures into individual + arguments at runtime. The empty expression ``()`` is legal at the reader level, but has no inherent meaning. Trying to compile it is an error. For the empty tuple, use ``#()``. @@ -407,12 +461,9 @@ A format string (or "f-string", or "formatted string literal") is a string literal with embedded code, possibly accompanied by formatting commands. The result is an :class:`FString `, Hy f-strings work much like :ref:`Python f-strings ` except that the embedded code is in Hy -rather than Python. - -:: +rather than Python. :: - => (print f"The sum is {(+ 1 1)}.") - The sum is 2. + (print f"The sum is {(+ 1 1)}.") ; => The sum is 2. Since ``=``, ``!``, and ``:`` are identifier characters in Hy, Hy decides where the code in a replacement field ends (and any debugging ``=``, conversion @@ -420,12 +471,9 @@ specifier, or format specifier begins) by parsing exactly one form. You can use ``do`` to combine several forms into one, as usual. Whitespace may be necessary to terminate the form:: - => (setv foo "a") - => (print f"{foo:x<5}") - … - NameError: name 'hyx_fooXcolonXxXlessHthan_signX5' is not defined - => (print f"{foo :x<5}") - axxxx + (setv foo "a") + (print f"{foo:x<5}") ; => NameError: name 'hyx_fooXcolonXxXlessHthan_signX5' is not defined + (print f"{foo :x<5}") ; => axxxx Unlike Python, whitespace is allowed between a conversion and a format specifier. @@ -447,18 +495,19 @@ Syntactic sugar is available to construct two-item :ref:`expressions by the reader, a new expression is created with the corresponding macro as the first element and the next parsed form as the second. No parentheses are required. Thus, since ``'`` is short for ``quote``, ``'FORM`` is read as -``(quote FORM)``. This is all resolved at the reader level, so the model that -gets produced is the same whether you take your code with sugar or without. +``(quote FORM)``. Whitespace is allowed, as in ``' FORM``. This is all resolved +at the reader level, so the model that gets produced is the same whether you +take your code with sugar or without. ========================== ================ Macro Syntax ========================== ================ -:hy:func:`quasiquote` ```FORM`` :hy:func:`quote` ``'FORM`` -:hy:func:`unpack-iterable` ``#* FORM`` -:hy:func:`unpack-mapping` ``#** FORM`` +:hy:func:`quasiquote` ```FORM`` :hy:func:`unquote` ``~FORM`` :hy:func:`unquote-splice` ``~@FORM`` +:hy:func:`unpack-iterable` ``#* FORM`` +:hy:func:`unpack-mapping` ``#** FORM`` ========================== ================ Reader macros diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 03f25c76c..929304731 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -65,11 +65,13 @@ the **form**. ``92``, ``*``, and ``(* 92 2)`` are all forms. A Lisp program consists of a sequence of forms nested within forms. Forms are typically separated from each other by whitespace, but some forms, such as string literals (``"Hy, world!"``), can contain whitespace themselves. An -**expression** is a form enclosed in parentheses; its first child form, called -the **head**, determines what the expression does, and should generally be a -function or macro. Functions are the most ordinary sort of head, whereas macros -(described in more detail below) are functions executed at compile-time instead -and return code to be executed at run-time. +:ref:`expression ` is a form enclosed in parentheses; its first +child form, called the **head**, determines what the expression does, and +should generally be a function or macro. :py:term:`Functions `, the +most ordinary sort of head, constitute reusable pieces of code that can take in +arguments and return a value. Macros (described in more detail :ref:`below +`) are a special kind of function that's executed at +compile-time and returns code to be executed at run-time. Comments start with a ``;`` character and continue till the end of the line. A comment is functionally equivalent to whitespace. :: @@ -77,8 +79,7 @@ comment is functionally equivalent to whitespace. :: (setv password "susan") ; My daughter's name Although ``#`` isn't a comment character in Hy, a Hy program can begin with a -`shebang line `_, which Hy itself -will ignore:: +:ref:`shebang line `, which Hy itself will ignore:: #!/usr/bin/env hy (print "Make me executable, and run me!") @@ -261,6 +262,8 @@ Python can import a Hy module like any other module so long as Hy itself has been imported first, which, of course, must have already happened if you're running a Hy program. +.. _tutorial-macros: + Macros ====== @@ -278,7 +281,9 @@ program. Here's a simple example:: (print "Value:" (m)) (print "Done executing") -If you run this program twice in a row, you'll see this:: +If you run this program twice in a row, you'll see this: + +.. code-block:: text $ hy example.hy Now for a slow computation @@ -301,14 +306,15 @@ simply:: (print "Value:" 1) (print "Done executing") -Our macro ``m`` has an especially simple return value, an integer, which at -compile-time is converted to an integer literal. In general, macros can return -arbitrary Hy forms to be executed as code. There are several special operators -and macros that make it easy to construct forms programmatically, such as -:hy:func:`quote` (``'``), :hy:func:`quasiquote` (`````), :hy:func:`unquote` -(``~``), and :hy:func:`defmacro! `. The previous -chapter has :ref:`a simple example ` of using ````` and ``~`` to -define a new control construct ``do-while``. +Our macro ``m`` has an especially simple return value, an integer (:py:class:`int`), which at +compile-time is converted to an integer model (:class:`hy.models.Integer`). In general, macros can return +arbitrary Hy forms to be executed as code. There are several helper macros that +make it easy to construct forms programmatically, such as :hy:func:`quote` +(``'``), :hy:func:`quasiquote` (`````), :hy:func:`unquote` (``~``), +:hy:func:`unquote-splice` (``~@``), and :hy:func:`defmacro! +`. The previous chapter has :ref:`a simple example +` of using ````` and ``~@`` to define a new control construct +``do-while``. What if you want to use a macro that's defined in a different module? ``import`` won't help, because it merely translates to a Python ``import`` @@ -365,9 +371,14 @@ Next steps You now know enough to be dangerous with Hy. You may now smile villainously and sneak off to your Hydeaway to do unspeakable things. -Refer to Python's documentation for the details of Python semantics, and the -rest of this manual for Hy-specific features. Like Hy itself, the manual is -incomplete, but :ref:`contributions ` are always welcome. +Refer to Python's documentation for the details of Python semantics. In +particular, :ref:`the Python tutorial ` can be helpful even if +you have no interest in writing your own Python code, because it will introduce +you to the semantics, and you'll need a reading knowledge of Python syntax to +understand example code for Python libraries. + +Refer to the rest of this manual for Hy-specific features. Like Hy itself, the +manual is incomplete, but :ref:`contributions ` are always welcome. Bear in mind that Hy is still unstable, and with each release along the way to Hy 1.0, there are new breaking changes. Refer to `the NEWS file diff --git a/docs/whyhy.rst b/docs/whyhy.rst index c6c00c898..861823292 100644 --- a/docs/whyhy.rst +++ b/docs/whyhy.rst @@ -2,6 +2,8 @@ Why Hy? ======= +.. Changes to the below paragraph should be mirrored on Hy's homepage. + Hy (or "Hylang" for long; named after the insect order Hymenoptera, since Paul Tagliamonte was studying swarm behavior when he created the language) is a multi-paradigm general-purpose programming language in @@ -20,19 +22,27 @@ Hy versus Python The first thing a Python programmer will notice about Hy is that it has Lisp's traditional parenthesis-heavy prefix syntax in place of Python's C-like infix -syntax. For example, ``print("The answer is", 2 + object.method(arg))`` could -be written ``(print "The answer is" (+ 2 (.method object arg)))`` in Hy. -Consequently, Hy is free-form: structure is indicated by punctuation rather +syntax. For example, + +.. code-block:: python + + print("The answer is", 2 + object.method(arg)) + +could be written :: + + (print "The answer is" (+ 2 (.method object arg))) + +in Hy. Consequently, Hy is free-form: structure is indicated by punctuation rather than whitespace, making it convenient for command-line use. As in other Lisps, the value of a simplistic syntax is that it facilitates Lisp's signature feature: `metaprogramming -`_ through macros, which are -functions that manipulate code objects at compile time to produce new code -objects, which are then executed as if they had been part of the original code. -In fact, Hy allows arbitrary computation at compile-time. For example, here's a -simple macro that implements a C-style do-while loop, which executes its body -for as long as the condition is true, but at least once. +`_ through :doc:`macros +`, which are functions that manipulate code objects at compile time to +produce new code objects, which are then executed as if they had been part of +the original code. In fact, Hy allows arbitrary computation at compile-time. For +example, here's a simple macro that implements a C-style do-while loop, which +executes its body for as long as the condition is true, but at least once. .. _do-while: @@ -40,9 +50,9 @@ for as long as the condition is true, but at least once. (defmacro do-while [condition #* body] `(do - ~body + ~@body (while ~condition - ~body))) + ~@body))) (setv x 0) (do-while x @@ -50,7 +60,7 @@ for as long as the condition is true, but at least once. Hy also removes Python's restrictions on mixing expressions and statements, allowing for more direct and functional code. For example, Python doesn't allow -:ref:`with ` blocks, which close a resource once you're done using it, +:keyword:`with` blocks, which close a resource once you're done using it, to return values. They can only execute a set of statements: .. code-block:: python @@ -68,7 +78,7 @@ it like an ordinary function call:: (len (with [o (open "foo")] (.read o))) (len (with [o (open "bar")] (.read o))))) -To be even more concise, you can put a ``with`` form in a :hy:func:`gfor `:: +To be even more concise, you can put a ``with`` form in a :hy:func:`gfor`:: (print (sum (gfor filename ["foo" "bar"] @@ -79,9 +89,9 @@ Operators can be given more than two arguments (e.g., ``(+ 1 2 3)``), including augmented assignment operators (e.g., ``(+= x 1 2 3)``). They are also provided as ordinary first-class functions of the same name, allowing them to be passed to higher-order functions: ``(sum xs)`` could be written ``(reduce + xs)``, -after importing the function ``+`` from the module ``hy.pyops``. +after importing the function ``+`` from the module :hy:mod:`hy.pyops`. -The Hy compiler works by reading Hy source code into Hy model objects and +The Hy compiler works by reading Hy source code into Hy :ref:`model objects ` and compiling the Hy model objects into Python abstract syntax tree (:py:mod:`ast`) objects. Python AST objects can then be compiled and run by Python itself, byte-compiled for faster execution later, or rendered into Python source code. @@ -109,8 +119,8 @@ in Hy:: (import cherrypy) (defclass HelloWorld [] - #@(cherrypy.expose (defn index [self] - "Hello World!"))) + (defn [cherrypy.expose] index [self] + "Hello World!")) (cherrypy.quickstart (HelloWorld)) @@ -129,8 +139,9 @@ in a set literal. However, models can be concatenated and indexed just like plain lists, and you can return ordinary Python types from a macro or give them to :hy:func:`hy.eval` and Hy will automatically promote them to models. -Hy takes much of its semantics from Python. For example, Hy is a Lisp-1 because -Python functions use the same namespace as objects that aren't functions. In +Hy takes much of its semantics from Python. For example, functions use the same +namespace as objects that aren't functions, so a variable named ``globals`` +can shadow the Python built-in function :py:func:`globals`. In general, any Python code should be possible to literally translate to Hy. At the same time, Hy goes to some lengths to allow you to do typical Lisp things that aren't straightforward in Python. For example, Hy provides the @@ -139,8 +150,20 @@ aforementioned mixing of statements and expressions, :ref:`name mangling Python-legal identifiers, and a :hy:func:`let` macro to provide block-level scoping in place of Python's usual function-level scoping. -Overall, Hy, like Common Lisp, is intended to be an unopinionated big-tent -language that lets you do what you want. If you're interested in a more -small-and-beautiful approach to Lisp, in the style of Scheme, check out -`Hissp `_, another Lisp embedded in Python -that was created by a Hy developer. + +What Hy is not +-------------- + +Hy isn't minimal or elegant. Hy is big and ugly and proud of it; it's an +unopinionated big-tent language that lets you do what you want. It has all +of Python's least-motivated semantic features, plus more features, plus +various kinds of syntactic sugar. (The syntax isn't as complex as +Python's, but there are a lot of details beyond plain old S-expressions.) +If you're interested in a more small-and-beautiful approach to Lisp, in +the style of Scheme, check out `Hissp `_, +another Lisp embedded in Python that was created by a Hy developer. + +Also, Hy isn't a reimplementation of an older Lisp. It is its own +language. It looks kind of like Clojure and kind of like Common Lisp, but +nontrivial programs that run in one of these langauges can't be expected +to run on another unaltered. diff --git a/fastentrypoints.py b/fastentrypoints.py index 4369fbecd..c26a4ff64 100644 --- a/fastentrypoints.py +++ b/fastentrypoints.py @@ -24,7 +24,7 @@ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" +''' Monkey patch setuptools to write faster console_scripts with this format: import sys @@ -35,12 +35,10 @@ (c) 2016, Aaron Christianson http://github.com/ninjaaron/fast-entry_points -""" -import re - +''' from setuptools.command import easy_install - -TEMPLATE = """\ +import re +TEMPLATE = r''' # -*- coding: utf-8 -*- # EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}' __requires__ = '{3}' @@ -51,7 +49,8 @@ if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit({2}())""" + sys.exit({2}()) +'''.lstrip() @classmethod @@ -64,15 +63,15 @@ def get_args(cls, dist, header=None): # noqa: D205,D400 # pylint: disable=E1101 header = cls.get_header() spec = str(dist.as_requirement()) - for type_ in "console", "gui": - group = type_ + "_scripts" + for type_ in 'console', 'gui': + group = type_ + '_scripts' for name, ep in dist.get_entry_map(group).items(): # ensure_safe_name - if re.search(r"[\\/]", name): + if re.search(r'[\\/]', name): raise ValueError("Path separators not allowed in script names") script_text = TEMPLATE.format( - ep.module_name, ep.attrs[0], ".".join(ep.attrs), spec, group, name - ) + ep.module_name, ep.attrs[0], '.'.join(ep.attrs), + spec, group, name) # pylint: disable=E1101 args = cls._get_script_args(type_, name, header, script_text) for res in args: @@ -85,32 +84,29 @@ def get_args(cls, dist, header=None): # noqa: D205,D400 def main(): import os - import re import shutil import sys - - dests = sys.argv[1:] or ["."] - filename = re.sub("\.pyc$", ".py", __file__) + dests = sys.argv[1:] or ['.'] + filename = re.sub(r'\.pyc$', '.py', __file__) for dst in dests: shutil.copy(filename, dst) - manifest_path = os.path.join(dst, "MANIFEST.in") - setup_path = os.path.join(dst, "setup.py") + manifest_path = os.path.join(dst, 'MANIFEST.in') + setup_path = os.path.join(dst, 'setup.py') # Insert the include statement to MANIFEST.in if not present - with open(manifest_path, "a+") as manifest: + with open(manifest_path, 'a+') as manifest: manifest.seek(0) manifest_content = manifest.read() - if "include fastentrypoints.py" not in manifest_content: - manifest.write( - ("\n" if manifest_content else "") + "include fastentrypoints.py" - ) + if 'include fastentrypoints.py' not in manifest_content: + manifest.write(('\n' if manifest_content else '') + + 'include fastentrypoints.py') # Insert the import statement to setup.py if not present - with open(setup_path, "a+") as setup: + with open(setup_path, 'a+') as setup: setup.seek(0) setup_content = setup.read() - if "import fastentrypoints" not in setup_content: + if 'import fastentrypoints' not in setup_content: setup.seek(0) setup.truncate() - setup.write("import fastentrypoints\n" + setup_content) + setup.write('import fastentrypoints\n' + setup_content) diff --git a/hy/__init__.py b/hy/__init__.py index fba3b9afc..afa43dcd4 100644 --- a/hy/__init__.py +++ b/hy/__init__.py @@ -15,6 +15,20 @@ def _initialize_env_var(env_var, default_val): hy.importer._inject_builtins() # we import for side-effects. + +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 ` 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): + import importlib + return importlib.import_module(module_name) + def __getattr__(self, s): + 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 # to be loaded if they're not needed. @@ -23,7 +37,7 @@ def _initialize_env_var(env_var, default_val): read_many="hy.reader", mangle="hy.reader", unmangle="hy.reader", - eval=["hy.compiler", "hy_eval"], + eval=["hy.compiler", "hy_eval_user"], repr=["hy.core.hy_repr", "hy_repr"], repr_register=["hy.core.hy_repr", "hy_repr_register"], gensym="hy.core.util", @@ -31,6 +45,7 @@ def _initialize_env_var(env_var, default_val): macroexpand_1="hy.core.util", disassemble="hy.core.util", as_model="hy.models", + REPL="hy.repl", ) diff --git a/hy/_compat.py b/hy/_compat.py index 815f3a5e5..4b83686fa 100644 --- a/hy/_compat.py +++ b/hy/_compat.py @@ -2,10 +2,12 @@ import platform import sys -PY3_8 = sys.version_info >= (3, 8) PY3_9 = sys.version_info >= (3, 9) PY3_10 = sys.version_info >= (3, 10) +PY3_11 = sys.version_info >= (3, 11) +PY3_12 = sys.version_info >= (3, 12) PYPY = platform.python_implementation() == "PyPy" +PYODIDE = platform.system() == "Emscripten" if not PY3_9: @@ -25,7 +27,7 @@ def rewriting_unparse(ast_obj): ast_obj = copy.deepcopy(ast_obj) for node in ast.walk(ast_obj): - if type(node) in (ast.Constant, ast.Str): + if type(node) is ast.Constant: # Don't touch string literals. continue for field in node._fields: @@ -39,44 +41,3 @@ def rewriting_unparse(ast_obj): return true_unparse(ast_obj) ast.unparse = rewriting_unparse - - -if not PY3_8: - # Shim `re.Pattern`. - import re - - re.Pattern = type(re.compile("")) - - -# Provide a function substitute for `CodeType.replace`. -if PY3_8: - - def code_replace(code_obj, **kwargs): - return code_obj.replace(**kwargs) - -else: - _code_args = [ - "co_" + c - for c in ( - "argcount", - "kwonlyargcount", - "nlocals", - "stacksize", - "flags", - "code", - "consts", - "names", - "varnames", - "filename", - "name", - "firstlineno", - "lnotab", - "freevars", - "cellvars", - ) - ] - - def code_replace(code_obj, **kwargs): - return type(code_obj)( - *(kwargs.get(k, getattr(code_obj, k)) for k in _code_args) - ) diff --git a/hy/cmdline.py b/hy/cmdline.py index 76f905c4b..a7d748008 100644 --- a/hy/cmdline.py +++ b/hy/cmdline.py @@ -1,438 +1,25 @@ import argparse import ast -import builtins -import code -import codeop -import hashlib import importlib import io -import linecache import os import platform import py_compile +import re import runpy import sys -import time -import traceback import types -from contextlib import contextmanager +from contextlib import nullcontext from pathlib import Path import hy from hy._compat import PY3_9 -from hy.compiler import HyASTCompiler, hy_ast_compile_flags, hy_compile, hy_eval -from hy.completer import Completer, completion -from hy.errors import ( - HyLanguageError, - HyMacroExpansionError, - HyRequireError, - filtered_hy_exceptions, - hy_exc_handler, -) -from hy.importer import HyLoader, runhy -from hy.macros import enable_readers, require, require_reader -from hy.reader import mangle, read_many -from hy.reader.exceptions import PrematureEndOfInput -from hy.reader.hy_reader import HyReader - -sys.last_type = None -sys.last_value = None -sys.last_traceback = None - - -class HyQuitter: - def __init__(self, name): - self.name = name - - def __repr__(self): - return "Use (%s) or Ctrl-D (i.e. EOF) to exit" % (self.name) - - __str__ = __repr__ - - def __call__(self, code=None): - try: - sys.stdin.close() - except: - pass - raise SystemExit(code) - - -class HyHelper: - def __repr__(self): - return ( - "Use (help) for interactive help, or (help object) for help " - "about object." - ) - - def __call__(self, *args, **kwds): - import pydoc - - return pydoc.help(*args, **kwds) - - -@contextmanager -def extend_linecache(add_cmdline_cache): - _linecache_checkcache = linecache.checkcache - - def _cmdline_checkcache(*args): - _linecache_checkcache(*args) - linecache.cache.update(add_cmdline_cache) - - linecache.checkcache = _cmdline_checkcache - yield - linecache.checkcache = _linecache_checkcache - - -_codeop_maybe_compile = codeop._maybe_compile - - -def _hy_maybe_compile(compiler, source, filename, symbol): - """The `codeop` version of this will compile the same source multiple - times, and, since we have macros and things like `eval-and-compile`, we - can't allow that. - """ - if not isinstance(compiler, HyCompile): - return _codeop_maybe_compile(compiler, source, filename, symbol) - - for line in source.split("\n"): - line = line.strip() - if line and line[0] != ";": - # Leave it alone (could do more with Hy syntax) - break - else: - if symbol != "eval": - # Replace it with a 'pass' statement (i.e. tell the compiler to do - # nothing) - source = "pass" - - return compiler(source, filename, symbol) - - -codeop._maybe_compile = _hy_maybe_compile - - -class HyCompile(codeop.Compile): - """This compiler uses `linecache` like - `IPython.core.compilerop.CachingCompiler`. - """ - - def __init__( - self, module, locals, ast_callback=None, hy_compiler=None, cmdline_cache={} - ): - self.module = module - self.locals = locals - self.ast_callback = ast_callback - self.hy_compiler = hy_compiler - self.reader = HyReader() - - super().__init__() - - if hasattr(self.module, "__reader_macros__"): - enable_readers( - self.module, self.reader, self.module.__reader_macros__.keys() - ) - - self.flags |= hy_ast_compile_flags - - self.cmdline_cache = cmdline_cache - - def _cache(self, source, name): - entry = ( - len(source), - time.time(), - [line + "\n" for line in source.splitlines()], - name, - ) - - linecache.cache[name] = entry - self.cmdline_cache[name] = entry - - def _update_exc_info(self): - self.locals["_hy_last_type"] = sys.last_type - self.locals["_hy_last_value"] = sys.last_value - # Skip our frame. - sys.last_traceback = getattr(sys.last_traceback, "tb_next", sys.last_traceback) - self.locals["_hy_last_traceback"] = sys.last_traceback - - def __call__(self, source, filename="", symbol="single"): - - if source == "pass": - # We need to return a no-op to signal that no more input is needed. - return (compile(source, filename, symbol),) * 2 - - hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest() - name = "{}-{}".format(filename.strip("<>"), hash_digest) - - self._cache(source, name) - - try: - root_ast = ast.Interactive if symbol == "single" else ast.Module - - # Our compiler doesn't correspond to a real, fixed source file, so - # we need to [re]set these. - self.hy_compiler.filename = name - self.hy_compiler.source = source - hy_ast = read_many( - source, filename=name, reader=self.reader, skip_shebang=True - ) - exec_ast, eval_ast = hy_compile( - hy_ast, - self.module, - root=root_ast, - get_expr=True, - compiler=self.hy_compiler, - filename=name, - source=source, - import_stdlib=False, - ) - - if self.ast_callback: - self.ast_callback(exec_ast, eval_ast) - - exec_code = super().__call__(exec_ast, name, symbol) - eval_code = super().__call__(eval_ast, name, "eval") - - except Exception as e: - # Capture and save the error before we handle further - - sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() - self._update_exc_info() - - if isinstance(e, (PrematureEndOfInput, SyntaxError)): - raise - else: - # Hy will raise exceptions during compile-time that Python would - # raise during run-time (e.g. import errors for `require`). In - # order to work gracefully with the Python world, we convert such - # Hy errors to code that purposefully reraises those exceptions in - # the places where Python code expects them. - # Capture a traceback without the compiler/REPL frames. - exec_code = super(HyCompile, self).__call__( - "raise _hy_last_value.with_traceback(_hy_last_traceback)", - name, - symbol, - ) - eval_code = super(HyCompile, self).__call__("None", name, "eval") - - return exec_code, eval_code - - -class HyCommandCompiler(codeop.CommandCompiler): - def __init__(self, *args, **kwargs): - self.compiler = HyCompile(*args, **kwargs) - - def __call__(self, *args, **kwargs): - try: - return super().__call__(*args, **kwargs) - except PrematureEndOfInput: - # We have to do this here, because `codeop._maybe_compile` won't - # take `None` for a return value (at least not in Python 2.7) and - # this exception type is also a `SyntaxError`, so it will be caught - # by `code.InteractiveConsole` base methods before it reaches our - # `runsource`. - return None - - -class HyREPL(code.InteractiveConsole): - "A subclass of :class:`code.InteractiveConsole` for Hy." - - def __init__(self, spy=False, output_fn=None, locals=None, filename=""): - - # Create a proper module for this REPL so that we can obtain it easily - # (e.g. using `importlib.import_module`). - # We let `InteractiveConsole` initialize `self.locals` when it's - # `None`. - super().__init__(locals=locals, filename=filename) - - module_name = self.locals.get("__name__", "__console__") - # Make sure our newly created module is properly introduced to - # `sys.modules`, and consistently use its namespace as `self.locals` - # from here on. - self.module = sys.modules.setdefault(module_name, types.ModuleType(module_name)) - self.module.__dict__.update(self.locals) - self.locals = self.module.__dict__ - - if os.environ.get("HYSTARTUP"): - try: - loader = HyLoader("__hystartup__", os.environ.get("HYSTARTUP")) - spec = importlib.util.spec_from_loader(loader.name, loader) - mod = importlib.util.module_from_spec(spec) - sys.modules.setdefault(mod.__name__, mod) - loader.exec_module(mod) - imports = mod.__dict__.get( - "__all__", - [name for name in mod.__dict__ if not name.startswith("_")], - ) - imports = {name: mod.__dict__[name] for name in imports} - spy = spy or imports.get("repl_spy") - output_fn = output_fn or imports.get("repl_output_fn") - - # Load imports and defs - self.locals.update(imports) - - # load module macros - require(mod, self.module, assignments="ALL") - require_reader(mod, self.module, assignments="ALL") - except Exception as e: - print(e) - - self.hy_compiler = HyASTCompiler(self.module, module_name) - - self.cmdline_cache = {} - self.compile = HyCommandCompiler( - self.module, - self.locals, - ast_callback=self.ast_callback, - hy_compiler=self.hy_compiler, - cmdline_cache=self.cmdline_cache, - ) - - self.spy = spy - self.last_value = None - self.print_last_value = True - - if output_fn is None: - self.output_fn = hy.repr - elif callable(output_fn): - self.output_fn = output_fn - elif "." in output_fn: - parts = [mangle(x) for x in output_fn.split(".")] - module, f = ".".join(parts[:-1]), parts[-1] - self.output_fn = getattr(importlib.import_module(module), f) - else: - self.output_fn = getattr(builtins, mangle(output_fn)) - - # Pre-mangle symbols for repl recent results: *1, *2, *3 - self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)] - self.locals.update({sym: None for sym in self._repl_results_symbols}) - - # Allow access to the running REPL instance - self.locals["_hy_repl"] = self - - # Compile an empty statement to load the standard prelude - exec_ast = hy_compile( - read_many(""), self.module, compiler=self.hy_compiler, import_stdlib=True - ) - if self.ast_callback: - self.ast_callback(exec_ast, None) - - def ast_callback(self, exec_ast, eval_ast): - if self.spy: - try: - # Mush the two AST chunks into a single module for - # conversion into Python. - new_ast = ast.Module( - exec_ast.body - + ([] if eval_ast is None else [ast.Expr(eval_ast.body)]), - type_ignores=[], - ) - print(ast.unparse(new_ast)) - except Exception: - msg = "Exception in AST callback:\n{}\n".format(traceback.format_exc()) - self.write(msg) - - def _error_wrap(self, error_fn, exc_info_override=False, *args, **kwargs): - sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() - - if exc_info_override: - # Use a traceback that doesn't have the REPL frames. - sys.last_type = self.locals.get("_hy_last_type", sys.last_type) - sys.last_value = self.locals.get("_hy_last_value", sys.last_value) - sys.last_traceback = self.locals.get( - "_hy_last_traceback", sys.last_traceback - ) - - sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) - - self.locals[mangle("*e")] = sys.last_value - - def showsyntaxerror(self, filename=None): - if filename is None: - filename = self.filename - self.print_last_value = False - - self._error_wrap( - super().showsyntaxerror, exc_info_override=True, filename=filename - ) - - def showtraceback(self): - self._error_wrap(super().showtraceback) - - def runcode(self, code): - try: - eval(code[0], self.locals) - self.last_value = eval(code[1], self.locals) - # Don't print `None` values. - self.print_last_value = self.last_value is not None - except SystemExit: - raise - except Exception as e: - # Set this to avoid a print-out of the last value on errors. - self.print_last_value = False - self.showtraceback() - - def runsource(self, source, filename="", symbol="exec"): - try: - res = super().runsource(source, filename, symbol) - except (HyMacroExpansionError, HyRequireError): - # We need to handle these exceptions ourselves, because the base - # method only handles `OverflowError`, `SyntaxError` and - # `ValueError`. - self.showsyntaxerror(filename) - return False - except (HyLanguageError): - # Our compiler will also raise `TypeError`s - self.showtraceback() - return False - - # Shift exisitng REPL results - if not res: - next_result = self.last_value - for sym in self._repl_results_symbols: - self.locals[sym], next_result = next_result, self.locals[sym] - - # Print the value. - if self.print_last_value: - try: - output = self.output_fn(self.last_value) - except Exception: - self.showtraceback() - return False - - print(output) - - return res - - def run(self): - "Start running the REPL. Return 0 when done." - - import colorama - - sys.ps1 = "=> " - sys.ps2 = "... " - - builtins.quit = HyQuitter("quit") - builtins.exit = HyQuitter("exit") - builtins.help = HyHelper() - - colorama.init() - - namespace = self.locals - with filtered_hy_exceptions(), extend_linecache(self.cmdline_cache), completion( - Completer(namespace) - ): - self.interact( - "Hy {version} using " - "{py}({build}) {pyversion} on {os}".format( - version=hy.__version__, - py=platform.python_implementation(), - build=platform.python_build()[0], - pyversion=platform.python_version(), - os=platform.system(), - ) - ) - - return 0 +from hy.compiler import hy_compile, hy_eval +from hy.errors import HyLanguageError, filtered_hy_exceptions, hy_exc_handler +from hy.importer import runhy +from hy.macros import require +from hy.reader import read_many +from hy.repl import REPL def set_path(filename): @@ -450,7 +37,7 @@ def run_command(source, filename=None): with filtered_hy_exceptions(): try: hy_eval( - read_many(source, filename=filename, skip_shebang=True), + read_many(source, filename=filename), __main__.__dict__, __main__, filename=filename, @@ -462,28 +49,7 @@ def run_command(source, filename=None): return 0 -def run_icommand(source, **kwargs): - if Path(source).exists(): - filename = source - set_path(source) - with open(source, "r", encoding="utf-8") as f: - source = f.read() - else: - filename = "" - - hr = HyREPL(**kwargs) - with filtered_hy_exceptions(): - res = hr.runsource(source, filename=filename) - - # If the command was prematurely ended, show an error (just like Python - # does). - if res: - hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback) - - return hr.run() - - -USAGE = "hy [-h | -v | -i CMD | -c CMD | -m MODULE | FILE | -] [ARG]..." +USAGE = "hy [-h | -v | -i | -c CMD | -m MODULE | FILE | -] [ARG]..." VERSION = "hy " + hy.__version__ EPILOG = """ FILE @@ -491,7 +57,7 @@ def run_icommand(source, **kwargs): - program read from stdin [ARG]... - arguments passed to program in sys.argv[1:] + arguments passed to program in (cut sys.argv 1) """ @@ -499,7 +65,7 @@ class HyArgError(Exception): pass -def cmdline_handler(scriptname, argv): +def cmdline_handler(argv): # We need to terminate interpretation of options after certain # options, such as `-c`. So, we can't use `argparse`. @@ -527,9 +93,8 @@ def cmdline_handler(scriptname, argv): ), dict( name=["-i"], - dest="icommand", - terminate=True, - help="program passed in as string, then stay in REPL", + action="store_true", + help="launch REPL after running script; forces a prompt even if stdin does not appear to be a terminal", ), dict( name=["-m"], @@ -655,42 +220,69 @@ def proc_opt(opt, arg=None, item=None, i=None): print(VERSION) return 0 - if "command" in options: + action, action_arg = ( + # If the `command` or `mod` options were provided, we'll run + # the corresponding code. + ["eval_string", options["command"]] + if "command" in options else + ["run_module", options["mod"]] + if "mod" in options else + # Otherwise, we'll run any provided filename as a script (or + # standard input, if the filename is "-"). + ["run_script_stdin", None] + if argv and argv[0] == "-" else + ["run_script_file", argv[0]] + if argv else + # With none of those arguments, we'll launch the REPL (if + # standard input is a TTY) or run a script from standard input + # (otherwise). + ["just_repl", None] + if sys.stdin.isatty() else + ["run_script_stdin", None]) + repl = ( + REPL( + spy = options.get("spy"), + output_fn = options.get("repl_output_fn")) + if "i" in options or action == "just_repl" + else None) + source = '' + + if action == "eval_string": sys.argv = ["-c"] + argv - return run_command(options["command"], filename="") - - if "mod" in options: + if repl: + source = action_arg + filename = '' + else: + return run_command(action_arg, filename="") + elif action == "run_module": + if repl: raise ValueError() set_path("") sys.argv = [program] + argv - runpy.run_module(hy.mangle(options["mod"]), run_name="__main__", alter_sys=True) + runpy.run_module(hy.mangle(action_arg), run_name="__main__", alter_sys=True) return 0 - - if "icommand" in options: - return run_icommand( - options["icommand"], - spy=options.get("spy"), - output_fn=options.get("repl_output_fn"), - ) - - if argv: - if argv[0] == "-": - # Read the program from stdin - return run_command(sys.stdin.read(), filename="") - + elif action == "run_script_stdin": + sys.argv = argv + if repl: + source = sys.stdin + filename = 'stdin' else: - # User did "hy " - - filename = Path(argv[0]) - set_path(filename) - # Ensure __file__ is set correctly in the code we're about - # to run. - if PY3_9 and not filename.is_absolute(): + return run_command(sys.stdin.read(), filename="") + elif action == "run_script_file": + sys.argv = argv + filename = Path(action_arg) + set_path(filename) + # Ensure __file__ is set correctly in the code we're about + # to run. + if PY3_9: + if not filename.is_absolute(): filename = Path.cwd() / filename - if PY3_9 and platform.system() == "Windows": + if platform.system() == "Windows": filename = os.path.normpath(filename) - + if repl: + source = Path(filename).read_text() + repl.compile.compiler.skip_next_shebang = True + else: try: - sys.argv = argv with filtered_hy_exceptions(): runhy.run_path(str(filename), run_name="__main__") return 0 @@ -705,15 +297,33 @@ def proc_opt(opt, arg=None, item=None, i=None): except HyLanguageError: hy_exc_handler(*sys.exc_info()) sys.exit(1) - - return HyREPL(spy=options.get("spy"), output_fn=options.get("repl_output_fn")).run() + else: + assert action == "just_repl" + + # If we didn't return earlier, we'll be using the REPL. + if source: + # Execute `source` in the REPL before entering interactive mode. + res = None + filename = str(filename) + with filtered_hy_exceptions(): + accum = '' + for chunk in ([source] if isinstance(source, str) else source): + accum += chunk + res = repl.runsource(accum, filename=filename) + if not res: + accum = '' + # If the command was prematurely ended, show an error (just like Python + # does). + if res: + hy_exc_handler(sys.last_type, sys.last_value, sys.last_traceback) + return repl.run() # entry point for cmd line script "hy" def hy_main(): sys.path.insert(0, "") try: - sys.exit(cmdline_handler("hy", sys.argv)) + sys.exit(cmdline_handler(sys.argv)) except HyArgError as e: print(e) exit(1) @@ -745,20 +355,83 @@ def hyc_main(): return rv +def hy2py_worker(source, options, filename=None, parent_module=None, output_filepath=None): + source_path = None + if isinstance(source, Path): + source_path = source + source = source.read_text(encoding="UTF-8") + if parent_module is None: + set_path(source_path) + + if not output_filepath and options.output: + output_filepath = options.output + + + with ( + open(output_filepath, "w", encoding="utf-8") + if output_filepath + else nullcontext() + ) as output_file: + + def printing_source(hst): + def _printing_gen(hst): + for node in hst: + if options.with_source: + print(node, file=output_file) + yield node + printing_hst = hy.models.Lazy(_printing_gen(hst)) + printing_hst.source = hst.source + printing_hst.filename = hst.filename + printing_hst.reader = hst.reader + return printing_hst + + hst = printing_source(read_many(source, filename, skip_shebang=True)) + + with filtered_hy_exceptions(): + module_name = source_path.stem if source_path else Path(filename).name + if parent_module: + module_name = f"{parent_module}.{module_name}" + module = types.ModuleType(module_name) + sys.modules[module_name] = module + try: + _ast = hy_compile( + hst, + module, + filename=filename, + source=source) + finally: + del sys.modules[module_name] + + if options.with_source: + print() + print() + + if options.with_ast: + print(ast.dump(_ast, **(dict(indent=2) if PY3_9 else {})), file=output_file) + print() + print() + + if not options.without_python: + print(ast.unparse(_ast), file=output_file) + + # entry point for cmd line script "hy2py" def hy2py_main(): options = dict( prog="hy2py", - usage="%(prog)s [options] [FILE]", + usage="%(prog)s [options] [-m MODULE | FILE | -]", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser = argparse.ArgumentParser(**options) - parser.add_argument( + gp = parser.add_argument_group().add_mutually_exclusive_group() + gp.add_argument("-m", dest="module", help="convert Hy module (or all files in module)") + gp.add_argument( "FILE", type=str, nargs="?", - help='Input Hy code (use STDIN if "-" or ' "not provided)", + help='convert Hy source file', ) + gp.add_argument("-", dest="use_stdin", action="store_true", help="read Hy from stdin") parser.add_argument( "--with-source", "-s", @@ -774,46 +447,59 @@ def hy2py_main(): action="store_true", help=("Do not show the Python code generated " "from the AST"), ) + parser.add_argument( + "--output", + "-o", + type=str, + nargs="?", + help="output file / directory", + ) options = parser.parse_args(sys.argv[1:]) - if options.FILE is None or options.FILE == "-": + if options.use_stdin or (options.FILE is None and options.module is None): sys.path.insert(0, "") filename = "" - source = sys.stdin.read() + hy2py_worker(sys.stdin.read(), options, filename) + elif options.module: + if options.module[:1] == ".": + raise ValueError( + "Relative module names not supported" + ) + sys.path.insert(0, "") + filename = options.module.replace(".", os.sep) + if os.path.isdir(filename): + # handle recursively if --output is specified + if not options.output: + raise ValueError( + f"{filename} is a directory but the output directory is not specified. Use --output or -o in command line arguments to specify the output directory." + ) + os.makedirs(options.output, exist_ok=True) + for path, _, files in os.walk(filename): + for name in files: + filename_raw, filename_ext = os.path.splitext(name) + if filename_ext == ".hy": + filepath = os.path.join(path, name) + # make sure to follow original file structure + subdirectory = os.path.relpath(path, filename) + output_directory_path = os.path.join( + options.output, subdirectory + ) + os.makedirs(output_directory_path, exist_ok=True) + hy2py_worker( + Path(filepath), + options, + parent_module=path.replace(os.sep, "."), + output_filepath=os.path.join( + output_directory_path, filename_raw + ".py" + ), + ) + else: + filename += ".hy" + parent_module = ".".join(options.module.split(".")[:-1]) + hy2py_worker(Path(filename), options, parent_module=parent_module) else: - filename = options.FILE - set_path(filename) - with open(options.FILE, "r", encoding="utf-8") as source_file: - source = source_file.read() - - def printing_source(hst): - for node in hst: - if options.with_source: - print(node) - yield node - - hst = hy.models.Lazy( - printing_source(read_many(source, filename, skip_shebang=True)) - ) - hst.source = source - hst.filename = filename - - with filtered_hy_exceptions(): - _ast = hy_compile(hst, "__main__", filename=filename, source=source) - - if options.with_source: - print() - print() - - if options.with_ast: - print(ast.dump(_ast, **(dict(indent=2) if PY3_9 else {}))) - print() - print() - - if not options.without_python: - print(ast.unparse(_ast)) - + hy2py_worker(Path(options.FILE), options, options.FILE) parser.exit(0) diff --git a/hy/compiler.py b/hy/compiler.py index 93dab5174..c47907def 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -1,15 +1,16 @@ import ast +import builtins import copy import importlib import inspect -import keyword import traceback import types +import warnings +from contextlib import contextmanager from funcparserlib.parser import NoParseError, many import hy -from hy._compat import PY3_8 from hy.errors import HyCompileError, HyLanguageError, HySyntaxError from hy.macros import macroexpand from hy.model_patterns import FORM, KEYWORD, unpack @@ -33,8 +34,8 @@ as_model, is_unpack, ) -from hy.reader import mangle -from hy.scoping import ScopeGlobal +from hy.reader import mangle, HyReader +from hy.scoping import ResolveOuterVars, ScopeGlobal hy_ast_compile_flags = 0 @@ -318,10 +319,6 @@ def mkexpr(*items, **kwargs): return make_hy_model(Expression, items, kwargs.get("rest")) -def mklist(*items, **kwargs): - return make_hy_model(List, items, kwargs.get("rest")) - - def is_annotate_expression(model): return isinstance(model, Expression) and model and model[0] == Symbol("annotate") @@ -329,7 +326,7 @@ def is_annotate_expression(model): class HyASTCompiler: """A Hy-to-Python AST compiler""" - def __init__(self, module, filename=None, source=None): + def __init__(self, module, filename=None, source=None, extra_macros=None): """ Args: module (Union[str, types.ModuleType]): Module name or object in which the Hy tree is evaluated. @@ -338,9 +335,18 @@ def __init__(self, module, filename=None, source=None): debugging. source (Optional[str]): The source for the file, if any, being compiled. This is optional information for informative error messages and debugging. + extra_macros (Optional[dict]): More macros to use during lookup. They take precedence + over macros in `module`. """ self.anon_var_count = 0 self.temp_if = None + self.extra_macros = extra_macros or {} + + # Make a list of dictionaries with local compiler settings, + # such as the definitions of local macros. The last element is + # considered the top of the stack. + self.local_state_stack = [] + self.new_local_state() if not inspect.ismodule(module): self.module = importlib.import_module(module) @@ -360,11 +366,35 @@ def __init__(self, module, filename=None, source=None): # Hy expects this to be present, so we prep the module for Hy # compilation. - self.module.__dict__.setdefault("__macros__", {}) - self.module.__dict__.setdefault("__reader_macros__", {}) + self.module.__dict__.setdefault("_hy_macros", {}) + self.module.__dict__.setdefault("_hy_reader_macros", {}) self.scope = ScopeGlobal(self) + def new_local_state(self): + 'Add a new local state to the top of the stack.' + self.local_state_stack.append(dict(macros = {})) + + def is_in_local_state(self): + return len(self.local_state_stack) > 1 + + def get_local_option(self, key, default): + 'Get the topmost available value of a local-state setting.' + return next( + (s[key] + for s in reversed(self.local_state_stack) + if key in s), + default) + + def warn_on_core_shadow(self, name): + if ( + mangle(name) in getattr(builtins, "_hy_macros", {}) and + self.get_local_option('warn_on_core_shadow', True)): + warnings.warn( + f"New macro `{name}` will shadow the core macro of the same name", + RuntimeWarning + ) + def get_anon_var(self, base="_hy_anon_var"): self.anon_var_count += 1 return f"{base}_{self.anon_var_count}" @@ -514,8 +544,25 @@ def _nonconst(self, name): raise self._syntax_error(name, "Can't assign to constant") return name + def eval(self, model): + return hy_eval( + model, + locals = self.module.__dict__, + module = self.module, + filename = self.filename, + source = self.source, + import_stdlib = False) + + @contextmanager + def local_state(self): + self.new_local_state() + try: + yield + finally: + self.local_state_stack.pop() + @builds_model(Expression) - def compile_expression(self, expr, *, allow_annotation_expression=False): + def compile_expression(self, expr): # Perform macro expansions expr = macroexpand(expr, self.module, self) if isinstance(expr, (Result, ast.AST)): @@ -534,45 +581,42 @@ def compile_expression(self, expr, *, allow_annotation_expression=False): root = args.pop(0) func = None - if isinstance(root, Symbol) and root.startswith("."): - # (.split "test test") -> "test test".split() - # (.a.b.c x v1 v2) -> (.c (. x a b) v1 v2) -> x.a.b.c(v1, v2) - - # Get the method name (the last named attribute - # in the chain of attributes) - attrs = [ - Symbol(a).replace(root) if a else None for a in root.split(".")[1:] - ] - if not all(attrs): - raise self._syntax_error(expr, "cannot access empty attribute") - root = attrs.pop() - - # Get the object we're calling the method on - # (extracted with the attribute access DSL) - # Skip past keywords and their arguments. - try: - kws, obj, rest = ( - many(KEYWORD + FORM | unpack("mapping")) + FORM + many(FORM) - ).parse(args) - except NoParseError: + if ( + isinstance(root, Expression) + and len(root) >= 2 + and isinstance(root[0], Symbol) + and not str(root[0]).strip(".") + and root[1] == Symbol("None") + ): + # ((. None a1 a2) obj v1 v2) -> ((. obj a1 a2) v1 v2) + # (The reader already parsed `.a1.a2` as `(. None a1 a2)`.) + + # Find the object we're calling the method on. + i = 0 + while i < len(args): + if isinstance(args[i], Keyword): + if i == 0 and len(args) == 1: + break + i += 2 + elif is_unpack("iterable", args[i]): + raise self._syntax_error( + args[i], "can't call a method on an `unpack-iterable` form" + ) + elif is_unpack("mapping", args[i]): + i += 1 + else: + break + else: raise self._syntax_error(expr, "attribute access requires object") - # Reconstruct `args` to exclude `obj`. - args = [x for p in kws for x in p] + list(rest) - if is_unpack("iterable", obj): - raise self._syntax_error( - obj, "can't call a method on an unpacking form" - ) - func = self.compile(Expression([Symbol(".").replace(root), obj] + attrs)) - # And get the method - func += asty.Attribute( - root, value=func.force_expr, attr=mangle(root), ctx=ast.Load() + func = self.compile( + Expression([Symbol("."), args.pop(i), *root[2:]]).replace(root) ) if is_annotate_expression(root): # Flatten and compile the annotation expression. ann_expr = Expression(root + args).replace(root) - return self.compile_expression(ann_expr, allow_annotation_expression=True) + return self.compile_expression(ann_expr) if not func: func = self.compile(root) @@ -585,36 +629,17 @@ def compile_expression(self, expr, *, allow_annotation_expression=False): @builds_model(Integer, Float, Complex) def compile_numeric_literal(self, x): - f = {Integer: int, Float: float, Complex: complex}[type(x)] - return asty.Num(x, n=f(x)) + return asty.Constant(x, value = + {Integer: int, Float: float, Complex: complex}[type(x)](x)) @builds_model(Symbol) def compile_symbol(self, symbol): if symbol == Symbol("..."): - return ( - asty.Constant(symbol, value=Ellipsis) - if PY3_8 - else asty.Ellipsis(symbol) - ) - - if "." in symbol: - glob, local = symbol.rsplit(".", 1) + return asty.Constant(symbol, value=Ellipsis) - if not glob: - raise self._syntax_error( - symbol, - "cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. {attr})` or `(.{attr} )`)".format( - attr=local - ), - ) - - if not local: - raise self._syntax_error(symbol, "cannot access empty attribute") - - glob = Symbol(glob).replace(symbol) - ret = self.compile_symbol(glob) - - return asty.Attribute(symbol, value=ret, attr=mangle(local), ctx=ast.Load()) + # By this point, `symbol` should be either all dots or + # dot-free. + assert not symbol.strip(".") or "." not in symbol if mangle(symbol) in ("None", "False", "True"): return asty.Constant(symbol, value=ast.literal_eval(mangle(symbol))) @@ -637,16 +662,15 @@ def compile_keyword(self, obj): attr="Keyword", ctx=ast.Load(), ), - args=[asty.Str(obj, s=obj.name)], + args=[asty.Constant(obj, value=obj.name)], keywords=[], ) return ret @builds_model(String, Bytes) def compile_string(self, string): - node = asty.Bytes if type(string) is Bytes else asty.Str - f = bytes if type(string) is Bytes else str - return node(string, s=f(string)) + return asty.Constant(string, value = + (bytes if type(string) is Bytes else str)(string)) @builds_model(FComponent) def compile_fcomponent(self, fcomponent): @@ -696,10 +720,7 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False): module = getattr(compiler, "module", None) or module if isinstance(module, str): - if module.startswith("<") and module.endswith(">"): - module = types.ModuleType(module) - else: - module = importlib.import_module(mangle(module)) + module = importlib.import_module(mangle(module)) if calling_frame and not module: module = calling_module(n=2) @@ -712,82 +733,18 @@ def get_compiler_module(module=None, compiler=None, calling_frame=False): def hy_eval( hytree, - locals=None, + locals, module=None, - ast_callback=None, compiler=None, filename=None, source=None, import_stdlib=True, + globals=None, + extra_macros=None, ): - """Evaluates a quoted expression and returns the value. - - If you're evaluating hand-crafted AST trees, make sure the line numbers - are set properly. Try `fix_missing_locations` and related functions in the - Python `ast` library. - - Examples: - :: - - => (hy.eval '(print "Hello World")) - "Hello World" - - If you want to evaluate a string, use ``read-str`` to convert it to a - form first:: - - => (hy.eval (hy.read-str "(+ 1 1)")) - 2 - - Args: - hytree (Object): - The Hy AST object to evaluate. - - locals (Optional[dict]): - Local environment in which to evaluate the Hy tree. Defaults to the - calling frame. - - module (Optional[Union[str, types.ModuleType]]): - Module, or name of the module, to which the Hy tree is assigned and - the global values are taken. - The module associated with `compiler` takes priority over this value. - When neither `module` nor `compiler` is specified, the calling frame's - module is used. - - ast_callback (Optional[Callable]): - A callback that is passed the Hy compiled tree and resulting - expression object, in that order, after compilation but before - evaluation. - - compiler (Optional[HyASTCompiler]): - An existing Hy compiler to use for compilation. Also serves as - the `module` value when given. - - filename (Optional[str]): - The filename corresponding to the source for `tree`. This will be - overridden by the `filename` field of `tree`, if any; otherwise, it - defaults to "". When `compiler` is given, its `filename` field - value is always used. - - source (Optional[str]): - A string containing the source code for `tree`. This will be - overridden by the `source` field of `tree`, if any; otherwise, - if `None`, an attempt will be made to obtain it from the module given by - `module`. When `compiler` is given, its `source` field value is always - used. - - Returns: - Any: Result of evaluating the Hy compiled tree. - """ module = get_compiler_module(module, compiler, True) - if locals is None: - frame = inspect.stack()[1][0] - locals = inspect.getargvalues(frame).locals - - if not isinstance(locals, dict): - raise TypeError("Locals must be a dictionary") - # Does the Hy AST object come with its own information? filename = getattr(hytree, "filename", filename) or "" source = getattr(hytree, "source", source) @@ -800,16 +757,70 @@ def hy_eval( filename=filename, source=source, import_stdlib=import_stdlib, + extra_macros=extra_macros, ) - if ast_callback: - ast_callback(_ast, expr) + if globals is None: + globals = module.__dict__ # Two-step eval: eval() the body of the exec call - eval(ast_compile(_ast, filename, "exec"), module.__dict__, locals) + eval(ast_compile(_ast, filename, "exec"), globals, locals) # Then eval the expression context and return that - return eval(ast_compile(expr, filename, "eval"), module.__dict__, locals) + return eval(ast_compile(expr, filename, "eval"), globals, locals) + + +def hy_eval_user(model, globals = None, locals = None, module = None, macros = None): + # This function is advertised as `hy.eval`. + """An equivalent of Python's :func:`eval` for evaluating Hy code. The chief difference is that the first argument should be a :ref:`model ` rather than source text. If you have a string of source text you want to evaluate, convert it to a model first with :hy:func:`hy.read` or :hy:func:`hy.read-many`:: + + (hy.eval '(+ 1 1)) ; => 2 + (hy.eval (hy.read "(+ 1 1)")) ; => 2 + + The optional arguments ``globals`` and ``locals`` work as in the case of :func:`eval`. + + Another optional argument, ``module``, can be a module object or a string naming a module. The module's ``__dict__`` attribute can fill in for ``globals`` (and hence also for ``locals``) if ``module`` is provided but ``globals`` isn't, but the primary purpose of ``module`` is to control where macro calls are looked up. Without this argument, the calling module of ``hy.eval`` is used instead. :: + + (defmacro my-test-mac [] 3) + (hy.eval '(my-test-mac)) ; => 3 + (import hyrule) + (hy.eval '(my-test-mac) :module hyrule) ; NameError + (hy.eval '(list-n 3 1) :module hyrule) ; => [1 1 1] + + Finally, finer control of macro lookup can be achieved by passing in a dictionary of macros as the ``macros`` argument. The keys of this dictionary should be mangled macro names, and the values should be function objects to implement those macros. This is the same structure as is produced by :hy:func:`local-macros`, and in fact, ``(hy.eval … :macros (local-macros))`` is useful to make local macros visible to ``hy.eval``, which otherwise doesn't see them. :: + + (defn f [] + (defmacro lmac [] 1) + (hy.eval '(lmac)) ; NameError + (print (hy.eval '(lmac) :macros (local-macros)))) ; => 1 + (f) + + In any case, macros provided in this dictionary will shadow macros of the same name that are associated with the provided or implicit module. You can shadow a core macro, too, so be careful: there's no warning for this as there is in the case of :hy:func:`defmacro`.""" + + if locals is None: + locals = globals + hy_was = None + if locals and 'hy' in locals: + hy_was = (locals['hy'],) + try: + value = hy_eval( + hytree = model, + globals = globals, + locals = (inspect.getargvalues(inspect.stack()[1][0]).locals + if locals is None and module is None + else locals), + module = get_compiler_module(module, None, True), + extra_macros = macros) + finally: + if locals is not None: + if hy_was: + # Restore the old value of `hy`. + locals['hy'], = hy_was + else: + # Remove the implicitly added `hy` (if execution + # reached far enough to add it). + locals.pop('hy', None) + return value def hy_compile( @@ -821,6 +832,7 @@ def hy_compile( filename=None, source=None, import_stdlib=True, + extra_macros=None, ): """Compile a hy.models.Object tree into a Python AST Module. @@ -841,23 +853,16 @@ def hy_compile( if `None`, an attempt will be made to obtain it from the module given by `module`. When `compiler` is given, its `source` field value is always used. + extra_macros (Optional[dict]): Passed through to `HyASTCompiler`, if it's called. Returns: ast.AST: A Python AST tree """ module = get_compiler_module(module, compiler, False) - if isinstance(module, str): - if module.startswith("<") and module.endswith(">"): - module = types.ModuleType(module) - else: - module = importlib.import_module(mangle(module)) - - if not inspect.ismodule(module): - raise TypeError("Invalid module type: {}".format(type(module))) - filename = getattr(tree, "filename", filename) source = getattr(tree, "source", source) + reader = getattr(tree, "reader", None) tree = as_model(tree) if not isinstance(tree, Object): @@ -865,15 +870,21 @@ def hy_compile( "`tree` must be a hy.models.Object or capable of " "being promoted to one" ) - compiler = compiler or HyASTCompiler(module, filename=filename, source=source) + compiler = compiler or HyASTCompiler( + module, + filename = filename, + source = source, + extra_macros = extra_macros) - with compiler.scope: + with HyReader.using_reader(reader, create=False), compiler.scope: result = compiler.compile(tree) expr = result.force_expr if not get_expr: result += result.expr_as_stmt() + result.stmts = list(map(ResolveOuterVars().visit, result.stmts)) + body = [] if issubclass(root, ast.Module): @@ -881,7 +892,8 @@ def hy_compile( if ( result.stmts and isinstance(result.stmts[0], ast.Expr) - and isinstance(result.stmts[0].value, ast.Str) + and isinstance(result.stmts[0].value, ast.Constant) + and isinstance(result.stmts[0].value.value, str) ): body += [result.stmts.pop(0)] diff --git a/hy/completer.py b/hy/completer.py index ae8967652..5ede05bd5 100644 --- a/hy/completer.py +++ b/hy/completer.py @@ -4,11 +4,10 @@ import re import sys -import hy.macros from hy import mangle, unmangle # Lazily import `readline` to work around -# https://bugs.python.org/issue2675#msg265564 +# https://github.com/python/cpython/issues/46927#issuecomment-1093418916 readline = None @@ -46,10 +45,10 @@ def __init__(self, namespace={}): self.namespace = namespace self.path = [builtins.__dict__, namespace] - namespace.setdefault("__macros__", {}) - namespace.setdefault("__reader_macros__", {}) + namespace.setdefault("_hy_macros", {}) + namespace.setdefault("_hy_reader_macros", {}) - self.path.append(namespace["__macros__"]) + self.path.append(namespace["_hy_macros"]) def attr_matches(self, text): # Borrowed from IPython's completer @@ -123,6 +122,14 @@ def completion(completer=None): history = os.environ.get("HY_HISTORY", os.path.expanduser("~/.hy-history")) readline.parse_and_bind("set blink-matching-paren on") + # Save and clear any existing history. + history_was = [] + for _ in range(readline.get_current_history_length()): + history_was.append(readline.get_history_item(1)) + readline.remove_history_item(0) + # Yes, the first item is numbered 1 by one method and 0 by the + # other. + try: readline.read_history_file(history) except OSError: @@ -137,3 +144,7 @@ def completion(completer=None): readline.write_history_file(history) except OSError: pass + # Restore the previously saved history. + readline.clear_history() + for item in history_was: + readline.add_history(item) diff --git a/hy/core/hy_repr.hy b/hy/core/hy_repr.hy index 1ac05e92d..2df8611a9 100644 --- a/hy/core/hy_repr.hy +++ b/hy/core/hy_repr.hy @@ -8,61 +8,57 @@ (setv _registry {}) (defn hy-repr-register [types f [placeholder None]] - "``hy.repr-register`` lets you set the function that ``hy.repr`` calls to - represent a type. - - Examples: - :: - - => (hy.repr-register the-type fun) - - => (defclass C) - => (hy.repr-register C (fn [x] \"cuddles\")) - => (hy.repr [1 (C) 2]) - \"[1 cuddles 2]\" - - If the type of an object passed to ``hy.repr`` doesn't have a registered - function, ``hy.repr`` falls back on ``repr``. - - Registered functions often call ``hy.repr`` themselves. ``hy.repr`` will - automatically detect self-references, even deeply nested ones, and - output ``\"...\"`` for them instead of calling the usual registered - function. To use a placeholder other than ``\"...\"``, pass a string of - your choice to the keyword argument ``:placeholder`` of - ``hy.repr-register``. - - => (defclass Container [object] - ... (defn __init__ (fn [self value] - ... (setv self.value value)))) - => (hy.repr-register Container :placeholder \"HY THERE\" (fn [x] - ... (+ \"(Container \" (hy.repr x.value) \")\"))) - => (setv container (Container 5)) - => (setv container.value container) - => (print (hy.repr container)) - '(Container HY THERE)' - " + #[[``hy.repr-register`` lets you set the function that :hy:func:`hy.repr` calls to + represent a type:: + + (defclass C) + (hy.repr-register C (fn [x] "cuddles")) + (hy.repr [1 (C) 2]) ; => "[1 cuddles 2]" + + Registered functions often call ``hy.repr`` themselves. ``hy.repr`` will + automatically detect self-references, even deeply nested ones, and + output ``"..."`` for them instead of calling the usual registered + function. To use a placeholder other than ``"..."``, pass a string of + your choice as the ``placeholder`` argument:: + + (defclass Container) + (hy.repr-register Container :placeholder "HY THERE" + (fn [x] f"(Container {(hy.repr x.value)})")) + (setv container (Container)) + (setv container.value container) + (hy.repr container) ; => "(Container HY THERE)"]] + (for [typ (if (isinstance types list) types [types])] (setv (get _registry typ) #(f placeholder)))) (setv _quoting False) (setv _seen (set)) (defn hy-repr [obj] - "This function is Hy's equivalent of Python's built-in ``repr``. - It returns a string representing the input object in Hy syntax. + #[[This function is Hy's equivalent of Python's :func:`repr`. + It returns a string representing the input object in Hy syntax. :: + + (hy.repr [1 2 3]) ; => "[1 2 3]" + (repr [1 2 3]) ; => "[1, 2, 3]" Like ``repr`` in Python, ``hy.repr`` can round-trip many kinds of values. Round-tripping implies that given an object ``x``, - ``(hy.eval (hy.read-str (hy.repr x)))`` returns ``x``, or at least a value - that's equal to ``x``. + ``(hy.eval (hy.read (hy.repr x)))`` returns ``x``, or at least a + value that's equal to ``x``. A notable exception to round-tripping + is that if a model contains a non-model, the + latter will be promoted to a model in the output:: - Examples: - :: + (setv + x (hy.models.List [5]) + output (hy.repr x) + y (hy.eval (hy.read output))) + (print output) ; '[5] + (print (type (get x 0))) ; + (print (type (get y 0))) ; + + When ``hy.repr`` doesn't know how to represent an object, it falls + back on :func:`repr`. Use :hy:func:`hy.repr-register` to add your + own conversion function for a type instead.]] - => (hy.repr [1 2 3]) - \"[1 2 3]\" - => (repr [1 2 3]) - \"[1, 2, 3]\" - " (setv [f placeholder] (.get _registry (type obj) [_base-repr None])) (global _quoting) @@ -107,9 +103,34 @@ 'unquote-splice "~@" 'unpack-iterable "#* " 'unpack-mapping "#** "}) + (setv x0 (when x (get x 0))) + (setv x1 (when (> (len x) 1) (get x 1))) + (cond - (and x (in (get x 0) syntax)) - (+ (get syntax (get x 0)) (hy-repr (get x 1))) + + (and + (>= (len x) 3) + (all (gfor e x (is (type e) hy.models.Symbol))) + (or (= x0 '.) (and + (= x1 'None) + (not (.strip (str x0) "."))))) + (+ + (if (= x1 'None) (str x0) "") + (.join "." (map hy-repr (cut + x + (if (= x1 'None) 2 1) + None)))) + + (and (= (len x) 2) (in x0 syntax)) + (if (and + (= x0 'unquote) + (isinstance x1 hy.models.Symbol) + (.startswith x1 "@")) + ; This case is special because `~@b` would be wrongly + ; interpreted as `(unquote-splice b)` instead of `(unquote @b)`. + (+ "~ " (hy-repr x1)) + (+ (get syntax x0) (hy-repr x1))) + True (+ "(" (_cat x) ")")))) @@ -194,7 +215,7 @@ (hy-repr (.span x)) (hy-repr (.group x 0))))) (hy-repr-register re.Pattern (fn [x] - (setv flags (& x.flags (~ re.UNICODE))) + (setv flags (& x.flags (bnot re.UNICODE))) ; We remove re.UNICODE since it's redundant with the type ; of the pattern, and Python's `repr` omits it, too. (.format "(re.compile {}{})" diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 0ab4128e0..3732f65d5 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -29,17 +29,16 @@ With no arguments, ``cond`` returns ``None``. With an odd number of arguments, ``cond`` raises an error." + (defn _cond [args] + (if args + `(if ~(get args 0) + ~(get args 1) + ~(_cond (cut args 2 None))) + 'None)) (if (% (len args) 2) (raise (TypeError "`cond` needs an even number of arguments")) (_cond args))) -(defn _cond [args] - (if args - `(if ~(get args 0) - ~(get args 1) - ~(_cond (cut args 2 None))) - 'None)) - (defmacro when [test #* body] #[[Shorthand for ``(if test (do …) None)``. See :hy:func:`if`. For a logically negated version, see Hyrule's :hy:func:`unless `. @@ -51,7 +50,7 @@ (return panic))]] `(if ~test (do ~@body) None)) -(defmacro defreader [key #* body] +(defmacro defreader [_hy_compiler key #* body] "Define a new reader macro. Reader macros are expanded at read time and allow you to modify the behavior @@ -60,9 +59,8 @@ and its base class :py:class:`Reader ` for details regarding the available processing methods. - Reader macro names can be any symbol that does not start with a ``^`` and are - callable by prefixing the name with a ``#``. i.e. ``(defreader upper ...)`` is - called with ``#upper``. + Reader macro names can be any valid identifier and are callable by prefixing + the name with a ``#``. i.e. ``(defreader upper ...)`` is called with ``#upper``. Examples: @@ -88,25 +86,22 @@ => #slice a:(+ 1 2):\"column\" (slice 42 3 column) - See the :ref:`reader macros docs ` for more detailed + See the :ref:`reader macros docs ` for more detailed information on how reader macros work and are defined. " - (when (not (isinstance &compiler.scope hy.scoping.ScopeGlobal)) - (raise (&compiler._syntax-error - &compiler.this + (when (not (isinstance _hy_compiler.scope hy.scoping.ScopeGlobal)) + (raise (_hy_compiler._syntax-error + _hy_compiler.this f"Cannot define reader macro outside of global scope."))) (when (not (isinstance key hy.models.Symbol)) (raise (ValueError f"expected a name, but got {key}"))) - (when (.startswith key "^") - (raise (ValueError "reader macro cannot start with a ^"))) - (if (and body (isinstance (get body 0) hy.models.String)) (setv [docstr #* body] body) (setv docstr None)) - (setv dispatch-key (hy.mangle (+ "#" (str key)))) + (setv dispatch-key (str key)) `(do (eval-and-compile (hy.macros.reader-macro ~dispatch-key @@ -114,26 +109,63 @@ ~@(if docstr [docstr] []) ~@body))) (eval-when-compile - (setv (get hy.&reader.reader-table ~dispatch-key) - (get __reader_macros__ ~dispatch-key))))) - - -(defmacro doc [symbol] - "macro documentation - - Gets help for a macro function available in this module. - Use ``require`` to make other macros available. - - Use ``(help foo)`` instead for help with runtime objects." - (setv symbol (str symbol)) - (setv mangled (hy.mangle symbol)) - (setv builtins (hy.gensym "builtins")) - `(do (import builtins :as ~builtins) - (help (or (.get __macros__ ~mangled) - (.get __reader_macros__ ~mangled) - (.get (. ~builtins __macros__) ~mangled) - (.get (. ~builtins __reader_macros__) ~mangled) - (raise (NameError f"macro {~symbol !r} is not defined")))))) + (setv (get (. (hy.reader.HyReader.current-reader) reader-macros) ~dispatch-key) + (get _hy_reader_macros ~dispatch-key))))) + + +(defmacro get-macro [_hy_compiler arg1 [arg2 None]] + "Get the function object used to implement a macro. This works for all sorts of macros: core macros, global (i.e., module-level) macros, local macros, and reader macros. For regular (non-reader) macros, ``get-macro`` is called with one argument, a symbol or string literal, which can be premangled or not according to taste. For reader macros, this argument must be preceded by the literal keyword ``:reader`` (and note that the hash mark, ``#``, is not included in the name of the reader macro). :: + + (get-macro my-macro) + (get-macro :reader my-reader-macro) + + Except when retrieving a local macro, ``get-macro`` expands to a :hy:func:`get ` form on the appropriate object, such as ``_hy_macros``, selected at the time of expanding ``get-macro``. This means you can say ``(del (get-macro …))``, perhaps wrapped in :hy:func:`eval-and-compile` or :hy:func:`eval-when-compile`, to delete a macro, but it's easy to get confused by the order of evaluation and number of evaluations. For more predictable results in complex situations, use ``(del (get …))`` directly instead of ``(del (get-macro …))``." + + (import builtins) + (setv [name reader?] (cond + (= arg1 ':reader) + [(str arg2) True] + (isinstance arg1 hy.models.Expression) + [(hy.mangle (.join "." (cut arg1 1 None))) False] + True + [(hy.mangle arg1) False])) + (setv namespace (if reader? "_hy_reader_macros" "_hy_macros")) + (cond + (and (not reader?) (setx local (.get (_local-macros _hy_compiler) name))) + local + (in name (getattr _hy_compiler.module namespace {})) + `(get ~(hy.models.Symbol namespace) ~name) + (in name (getattr builtins namespace {})) + `(get (. hy.I.builtins ~(hy.models.Symbol namespace)) ~name) + True + (raise (NameError (.format "no such {}macro: {!r}" + (if reader? "reader " "") + name))))) + + +(defmacro local-macros [_hy_compiler] + #[[Expands to a dictionary mapping the mangled names of local macros to the function objects used to implement those macros. Thus, ``local-macros`` provides a rough local equivalent of ``_hy_macros``. :: + + (defn f [] + (defmacro m [] + "This is the docstring for the macro `m`." + 1) + (help (get (local-macros) "m"))) + (f) + + The equivalency is rough in the sense that ``local-macros`` returns a literal dictionary, not a preexisting object that Hy uses for resolving macro names. So, modifying the dictionary will have no effect. + + See also :hy:func:`get-macro `.]] + (_local-macros _hy_compiler)) + +(defn _local_macros [_hy_compiler] + (setv seen #{}) + (dfor + state _hy_compiler.local_state_stack + m (get state "macros") + :if (not-in m seen) + :do (.add seen m) + m (hy.models.Symbol (hy.macros.local-macro-name m)))) (defmacro export [#* args] @@ -166,30 +198,3 @@ (if (isinstance a hy.models.List) (lfor x a (hy.models.String x)) (raise (TypeError "arguments must be keywords or lists of symbols")))))))) - -(defmacro delmacro - [#* names] - #[[Delete a macro(s) from the current module - :: - - => (require a-module [some-macro]) - => (some-macro) - 1 - - => (delmacro some-macro) - => (some-macro) - Traceback (most recent call last): - File "", line 1, in - (some-macro) - NameError: name 'some_macro' is not defined - - => (delmacro some-macro) - Traceback (most recent call last): - File "", line 1, in - (delmacro some-macro) - NameError: macro 'some-macro' is not defined - ]] - (let [sym (hy.gensym)] - `(eval-and-compile - (for [~sym ~(lfor name names (hy.mangle name))] - (when (in ~sym __macros__) (del (get __macros__ ~sym))))))) diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py index cb043e0a8..7f82948c1 100644 --- a/hy/core/result_macros.py +++ b/hy/core/result_macros.py @@ -15,9 +15,10 @@ from funcparserlib.parser import finished, forward_decl, many, maybe, oneplus, some -from hy.compiler import Result, asty, hy_eval, mkexpr +from hy._compat import PY3_11, PY3_12 +from hy.compiler import Result, asty, mkexpr from hy.errors import HyEvalError, HyInternalError, HyTypeError -from hy.macros import pattern_macro, require, require_reader +from hy.macros import pattern_macro, require, require_reader, local_macro_name from hy.model_patterns import ( FORM, KEYWORD, @@ -53,10 +54,11 @@ String, Symbol, Tuple, + as_model, is_unpack, ) -from hy.reader import mangle, unmangle -from hy.scoping import ScopeFn, ScopeGen, ScopeLet, is_inside_function_scope +from hy.reader import mangle +from hy.scoping import OuterVar, ScopeFn, ScopeGen, ScopeLet, is_function_scope, is_inside_function_scope, nearest_python_scope # ------------------------------------------------ # * Helpers @@ -70,7 +72,37 @@ def pvalue(root, wanted): def maybe_annotated(target): - return pexpr(sym("annotate") + target + FORM) | target >> (lambda x: (x, None)) + return ( + pexpr(sym("annotate") + target + FORM).named('`annotate` form') | + (target >> (lambda x: (x, None)))) + + +def dotted(name): + return Expression(map(Symbol, [".", *name.split(".")])) + + +type_params = sym(":tp") + brackets(many( + maybe_annotated(SYM) | unpack("either", Symbol))) + +def digest_type_params(compiler, tp): + "Return a `type_params` attribute for `FunctionDef` etc." + + if tp: + if not PY3_12: + compiler._syntax_error(tp, "`:tp` requires Python 3.12 or later") + tp, = tp + elif not PY3_12: + return {} + + return dict(type_params = [ + asty.TypeVarTuple(x[1], name = mangle(x[1])) + if is_unpack("iterable", x) else + asty.ParamSpec(x[1], name = mangle(x[1])) + if is_unpack("mapping", x) else + asty.TypeVar(x[0], + name = mangle(x[0]), + bound = x[1] and compiler.compile(x[1]).force_expr) + for x in (tp or [])]) # ------------------------------------------------ @@ -79,23 +111,16 @@ def maybe_annotated(target): @pattern_macro("do", [many(FORM)]) -def compile_do(self, expr, root, body): - return self._compile_branch(body) +def compile_do(compiler, expr, root, body): + return compiler._compile_branch(body) -@pattern_macro(["eval-and-compile", "eval-when-compile"], [many(FORM)]) -def compile_eval_and_compile(compiler, expr, root, body): +@pattern_macro(["eval-and-compile", "eval-when-compile", "do-mac"], [many(FORM)]) +def compile_eval_foo_compile(compiler, expr, root, body): new_expr = Expression([Symbol("do").replace(expr[0])]).replace(expr) try: - hy_eval( - new_expr + body, - compiler.module.__dict__, - compiler.module, - filename=compiler.filename, - source=compiler.source, - import_stdlib=False, - ) + value = compiler.eval(new_expr + body) except HyInternalError: # Unexpected "meta" compilation errors need to be treated # like normal (unexpected) compilation errors at this level @@ -110,8 +135,10 @@ def compile_eval_and_compile(compiler, expr, root, body): raise HyEvalError(str(e), compiler.filename, body, compiler.source) return ( - compiler._compile_branch(body) - if mangle(root) == "eval_and_compile" + compiler.compile(as_model(value)) + if root == "do-mac" + else compiler._compile_branch(body) + if root == "eval-and-compile" else Result() ) @@ -123,7 +150,7 @@ def compile_inline_python(compiler, expr, root, code): try: o = asty.parse( expr, - textwrap.dedent(code) if exec_mode else code, + textwrap.dedent(code) if exec_mode else "(" + code + "\n)", compiler.filename, "exec" if exec_mode else "eval", ).body @@ -142,11 +169,9 @@ def compile_inline_python(compiler, expr, root, code): @pattern_macro(["quote", "quasiquote"], [FORM]) def compile_quote(compiler, expr, root, arg): - level = Inf if root == "quote" else 0 # Only quasiquotes can unquote - stmts, _ = render_quoted_form(compiler, arg, level) - ret = compiler.compile(stmts) - return ret - + return compiler.compile(render_quoted_form(compiler, arg, + level = Inf if root == "quote" else 0)[0]) + # Only quasiquotes can unquote def render_quoted_form(compiler, form, level): """ @@ -164,24 +189,21 @@ def render_quoted_form(compiler, form, level): op = None if isinstance(form, Expression) and form and isinstance(form[0], Symbol): - op = unmangle(mangle(form[0])) - if level == 0 and op in ("unquote", "unquote-splice"): - if len(form) != 2: - raise HyTypeError( - "`%s' needs 1 argument, got %s" % op, - len(form) - 1, - compiler.filename, - form, - compiler.source, - ) - return form[1], op == "unquote-splice" - elif op == "quasiquote": - level += 1 - elif op in ("unquote", "unquote-splice"): - level -= 1 - - hytype = form.__class__ - name = ".".join((hytype.__module__, hytype.__name__)) + op = mangle(form[0]).replace('_', '-') + if op in ("unquote", "unquote-splice", "quasiquote"): + if level == 0 and op != "quasiquote": + if len(form) != 2: + raise HyTypeError( + "`%s' needs 1 argument, got %s" % op, + len(form) - 1, + compiler.filename, + form, + compiler.source, + ) + return form[1], op == "unquote-splice" + level += 1 if op == "quasiquote" else -1 + + name = form.__class__.__name__ body = [form] if isinstance(form, Sequence): @@ -189,6 +211,8 @@ def render_quoted_form(compiler, form, level): for x in form: f_contents, splice = render_quoted_form(compiler, x, level) if splice: + if is_unpack("iterable", f_contents): + raise compiler._syntax_error(f_contents, "`unpack-iterable` is not allowed here") f_contents = Expression( [ Symbol("unpack-iterable"), @@ -199,22 +223,21 @@ def render_quoted_form(compiler, form, level): body = [List(contents)] if isinstance(form, FString) and form.brackets is not None: - body.extend([Keyword("brackets"), form.brackets]) + body.extend([Keyword("brackets"), String(form.brackets)]) elif isinstance(form, FComponent) and form.conversion is not None: body.extend([Keyword("conversion"), String(form.conversion)]) elif isinstance(form, Symbol): - body = [String(form)] + body = [String(form), Keyword("from_parser"), Symbol("True")] elif isinstance(form, Keyword): - body = [String(form.name)] + body = [String(form.name), Keyword("from_parser"), Symbol("True")] elif isinstance(form, String): if form.brackets is not None: - body.extend([Keyword("brackets"), form.brackets]) + body.extend([Keyword("brackets"), String(form.brackets)]) - ret = Expression([Symbol(name), *body]).replace(form) - return ret, False + return (Expression([dotted("hy.models." + name), *body]).replace(form), False) # ------------------------------------------------ @@ -222,9 +245,9 @@ def render_quoted_form(compiler, form, level): # ------------------------------------------------ -@pattern_macro(["not", "~"], [FORM], shadow=True) +@pattern_macro(["not", "bnot"], [FORM], shadow=True) def compile_unary_operator(compiler, expr, root, arg): - ops = {"not": ast.Not, "~": ast.Invert} + ops = {"not": ast.Not, "bnot": ast.Invert} operand = compiler.compile(arg) return operand + asty.UnaryOp(expr, op=ops[root](), operand=operand.force_expr) @@ -233,48 +256,71 @@ def compile_unary_operator(compiler, expr, root, arg): def compile_logical_or_and_and_operator(compiler, expr, operator, args): ops = {"and": (ast.And, True), "or": (ast.Or, None)} opnode, default = ops[operator] - osym = expr[0] if len(args) == 0: - return asty.Constant(osym, value=default) - elif len(args) == 1: - return compiler.compile(args[0]) - ret = Result() - values = list(map(compiler.compile, args)) - if any(value.stmts for value in values): - # Compile it to an if...else sequence - var = compiler.get_anon_var() - name = asty.Name(osym, id=var, ctx=ast.Store()) - expr_name = asty.Name(osym, id=var, ctx=ast.Load()) - temp_variables = [name, expr_name] - - def make_assign(value, node=None): - positioned_name = asty.Name(node or osym, id=var, ctx=ast.Store()) - temp_variables.append(positioned_name) - return asty.Assign(node or osym, targets=[positioned_name], value=value) - - current = root = [] - for i, value in enumerate(values): - if value.stmts: - node = value.stmts[0] - current.extend(value.stmts) + return asty.Constant(expr[0], value=default) + + ret = None + var = None # A temporary variable for assigning results to + assignment = None # The current assignment to `var` + stmts = None # The current statement list + can_append = False # Whether the current expression is the compiled boolop + + def put(node, value): + # Save the result of the operation so far to `var`. + nonlocal var, assignment, can_append + if var is None: + var = compiler.get_anon_var() + name = asty.Name(node, id=var, ctx=ast.Store()) + ret.temp_variables.append(name) + can_append = False + return (assignment := asty.Assign(node, targets=[name], value=value)) + + def get(node): + # Get the value of `var`, creating it if necessary. + if var is None: + stmts.append(put(node, ret.force_expr)) + name = asty.Name(node, id=var, ctx=ast.Load()) + ret.temp_variables.append(name) + return name + + for value in map(compiler.compile, args): + if ret is None: + # This is the first iteration. Don't actually introduce a + # `BoolOp` yet; the unary case doesn't need it. + ret = value + stmts = ret.stmts + can_append = False + elif value.stmts: + # Save the result of the statements to the temporary + # variable. Use an `if` statement to implement + # short-circuiting from this point. + node = value.stmts[0] + cond = get(node) + if operator == "or": + # Negate the conditional. + cond = asty.UnaryOp(node, op=ast.Not(), operand=cond) + branch = asty.If(node, test=cond, body=value.stmts, orelse=[]) + stmts.append(branch) + stmts = branch.body + stmts.append(put(node, value.force_expr)) + else: + # Add this value to the current `BoolOp`, or create a new + # one if we don't have one. + value = value.force_expr + def enbool(expr): + nonlocal can_append + if can_append: + expr.values.append(value) + return expr + can_append = True + return asty.BoolOp(expr, op=opnode(), values=[expr, value]) + if assignment: + assignment.value = enbool(assignment.value) else: - node = value.expr - current.append(make_assign(value.force_expr, value.force_expr)) - if i == len(values) - 1: - # Skip a redundant 'if'. - break - if operator == "and": - cond = expr_name - elif operator == "or": - cond = asty.UnaryOp(node, op=ast.Not(), operand=expr_name) - current.append(asty.If(node, test=cond, body=[], orelse=[])) - current = current[-1].body - ret = sum(root, ret) - ret += Result(expr=expr_name, temp_variables=temp_variables) - else: - ret += asty.BoolOp( - osym, op=opnode(), values=[value.force_expr for value in values] - ) + ret.expr = enbool(ret.expr) + + if var: + ret.expr = get(expr) return ret @@ -348,7 +394,7 @@ def compile_chained_comparison(compiler, expr, root, arg1, args): def compile_maths_expression(compiler, expr, root, args): if len(args) == 0: # Return the identity element for this operator. - return asty.Num(expr, n=({"+": 0, "|": 0, "*": 1}[root])) + return asty.Constant(expr, value=({"+": 0, "|": 0, "*": 1}[root])) if len(args) == 1: if root == "/": @@ -446,7 +492,7 @@ def compile_assign( ld_name = compiler.compile(name) - if result.temp_variables and isinstance(name, Symbol) and "." not in name: + if result.temp_variables and isinstance(name, Symbol): result.rename(compiler, compiler._nonconst(name)) if not is_assignment_expr: # Throw away .expr to ensure that (setv ...) returns None. @@ -480,17 +526,23 @@ def compile_assign( return result -@pattern_macro(["global", "nonlocal"], [oneplus(SYM)]) +@pattern_macro(["global", "nonlocal"], [many(SYM)]) def compile_global_or_nonlocal(compiler, expr, root, syms): - node = asty.Global if root == "global" else asty.Nonlocal - ret = node(expr, names=[mangle(s) for s in syms]) + if not syms: + return asty.Pass(expr) + + names = [mangle(s) for s in syms] + if root == "global": + ret = asty.Global(expr, names=names) + else: + ret = OuterVar(expr, compiler.scope, names) try: compiler.scope.define_nonlocal(ret, root) except SyntaxError as e: raise compiler._syntax_error(expr, e.msg) - return ret if ret.names else Result() + return ret if syms else Result() @pattern_macro("del", [many(FORM)]) @@ -694,9 +746,9 @@ def compile_if(compiler, expr, _, cond, body, orel_expr): ) -@pattern_macro( - ["for"], [brackets(loopers), many(notpexpr("else")) + maybe(dolike("else"))] -) +@pattern_macro(["for"], [ + brackets(loopers, name = 'square-bracketed loop clauses'), + many(notpexpr("else")) + maybe(dolike("else"))]) @pattern_macro(["lfor", "sfor", "gfor"], [loopers, FORM]) @pattern_macro(["dfor"], [loopers, finished]) # Here `finished` is a hack replacement for FORM + FORM: @@ -712,7 +764,8 @@ def compile_comprehension(compiler, expr, root, parts, final): is_for = root == "for" ctx = nullcontext() if is_for else compiler.scope.create(ScopeGen) - with ctx as scope: + mac_con = nullcontext() if is_for else compiler.local_state() + with mac_con, ctx as scope: # Compile the parts. if is_for: @@ -887,6 +940,7 @@ def f(parts): ), body=stmts + f(parts).stmts, decorator_list=[], + **({"type_params": []} if PY3_12 else {}), ) # Immediately call the new function. Unless the user asked # for a generator, wrap the call in `[].__class__(...)` or @@ -1078,7 +1132,8 @@ def compile_with_expression(compiler, expr, root, args, body): | pexpr(keepsym("|"), many(_pattern)) | braces(many(LITERAL + _pattern), maybe(pvalue("unpack-mapping", SYM))) | pexpr( - notsym(".", "|", "unpack-mapping", "unpack-iterable"), + pexpr(keepsym("."), oneplus(SYM)) + | notsym(".", "|", "unpack-mapping", "unpack-iterable"), many(parse_if(lambda x: not isinstance(x, Keyword), _pattern)), many(KEYWORD + _pattern), ) @@ -1124,6 +1179,7 @@ def compile_match_expression(compiler, expr, root, subject, clauses): ), body=guard.stmts + [asty.Return(guard.expr, value=guard.expr)], decorator_list=[], + **({"type_params": []} if PY3_12 else {}), ) lifted_if_defs.append(guardret) guard = Result(expr=asty.parse(guard, f"{fname}()").body[0].value) @@ -1169,11 +1225,7 @@ def compile_pattern(compiler, pattern): value, value=compiler.compile(value).force_expr.value, ) - elif ( - isinstance(value, (String, Integer, Float, Complex, Bytes)) - or isinstance(value, Symbol) - and "." in value - ): + elif isinstance(value, (String, Integer, Float, Complex, Bytes)): return asty.MatchValue( value, value=compiler.compile(value).expr, @@ -1217,11 +1269,15 @@ def compile_pattern(compiler, pattern): ) ) elif isinstance(value, Expression): - root, args, kwargs = value + head, args, kwargs = value keywords, values = zip(*kwargs) if kwargs else ([], []) return asty.MatchClass( value, - cls=compiler.scope.access(asty.Name(root, id=mangle(root), ctx=ast.Load())), + cls=compiler.compile( + # `head` could be a symbol or a dotted form. + (head[:1] + head[1]).replace(head) + if type(head) is Expression + else head).expr, patterns=[compile_pattern(compiler, v) for v in args], kwd_attrs=[kwd.name for kwd in keywords], kwd_patterns=[compile_pattern(compiler, value) for value in values], @@ -1229,7 +1285,7 @@ def compile_pattern(compiler, pattern): elif isinstance(value, Keyword): return asty.MatchClass( value, - cls=compiler.compile(Symbol("hy.models.Keyword")).expr, + cls=compiler.compile(dotted("hy.models.Keyword")).expr, patterns=[ asty.MatchValue(value, value=asty.Constant(value, value=value.name)) ], @@ -1267,10 +1323,10 @@ def compile_raise_expression(compiler, expr, root, exc, cause): @pattern_macro( "try", [ - many(notpexpr("except", "else", "finally")), + many(notpexpr("except", "except*", "else", "finally")), many( pexpr( - sym("except"), + keepsym("except") | keepsym("except*"), brackets() | brackets(FORM) | brackets(SYM, FORM), many(FORM), ) @@ -1280,15 +1336,72 @@ def compile_raise_expression(compiler, expr, root, exc, cause): ], ) def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbody): + if orelse is not None and not catchers: + # Python forbids `else` when there are no `except` clauses. + # But we can get the same effect by appending the `else` forms + # to the body. + body += list(orelse) + orelse = None body = compiler._compile_branch(body) + if not (catchers or finalbody): + # Python forbids this, so just return the body, per `do`. + return body return_var = asty.Name(expr, id=mangle(compiler.get_anon_var()), ctx=ast.Store()) handler_results = Result() handlers = [] + except_syms_seen = set() for catcher in catchers: - handler_results += compile_catch_expression( - compiler, catcher, return_var, *catcher + # exceptions catch should be either: + # [[list of exceptions]] + # or + # [variable [list of exceptions]] + # or + # [variable exception] + # or + # [exception] + # or + # [] + except_sym, exceptions, ebody = catcher + if not PY3_11 and except_sym == Symbol("except*"): + hy_compiler._syntax_error(except_sym, "`{}` requires Python 3.11 or later") + except_syms_seen.add(str(except_sym)) + if len(except_syms_seen) > 1: + raise compiler._syntax_error( + except_sym, "cannot have both `except` and `except*` on the same `try`" + ) + + name = None + if len(exceptions) == 2: + name = mangle(compiler._nonconst(exceptions[0])) + + exceptions_list = exceptions[-1] if exceptions else List() + if isinstance(exceptions_list, List): + if len(exceptions_list): + # [FooBar BarFoo] → catch Foobar and BarFoo exceptions + elts, types, _ = compiler._compile_collect(exceptions_list) + types += asty.Tuple(exceptions_list, elts=elts, ctx=ast.Load()) + else: + # [] → all exceptions caught + types = Result() + else: + types = compiler.compile(exceptions_list) + + # Create a "fake" scope for the exception variable. + # See: https://docs.python.org/3/reference/compound_stmts.html#the-try-statement + with compiler.scope.create(ScopeLet) as scope: + if name: + scope.add(name, name) + ebody = compiler._compile_branch(ebody) + ebody += asty.Assign(catcher, targets=[return_var], value=ebody.force_expr) + ebody += ebody.expr_as_stmt() + + handler_results += types + asty.ExceptHandler( + catcher, + type=types.expr, + name=name, + body=ebody.stmts or [asty.Pass(catcher)], ) handlers.append(handler_results.stmts.pop()) @@ -1307,15 +1420,6 @@ def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbo finalbody += finalbody.expr_as_stmt() finalbody = finalbody.stmts - # Using (else) without (except) is verboten! - if orelse and not handlers: - raise compiler._syntax_error(expr, "`try' cannot have `else' without `except'") - # Likewise a bare (try) or (try BODY). - if not (handlers or finalbody): - raise compiler._syntax_error( - expr, "`try' must have an `except' or `finally' clause" - ) - returnable = Result( expr=asty.Name(expr, id=return_var.id, ctx=ast.Load()), temp_variables=[return_var], @@ -1327,50 +1431,10 @@ def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbo ) body = body.stmts or [asty.Pass(expr)] - x = asty.Try(expr, body=body, handlers=handlers, orelse=orelse, finalbody=finalbody) - return handler_results + x + returnable - - -def compile_catch_expression(compiler, expr, var, exceptions, body): - # exceptions catch should be either: - # [[list of exceptions]] - # or - # [variable [list of exceptions]] - # or - # [variable exception] - # or - # [exception] - # or - # [] - - name = None - if len(exceptions) == 2: - name = mangle(compiler._nonconst(exceptions[0])) - - exceptions_list = exceptions[-1] if exceptions else List() - if isinstance(exceptions_list, List): - if len(exceptions_list): - # [FooBar BarFoo] → catch Foobar and BarFoo exceptions - elts, types, _ = compiler._compile_collect(exceptions_list) - types += asty.Tuple(exceptions_list, elts=elts, ctx=ast.Load()) - else: - # [] → all exceptions caught - types = Result() - else: - types = compiler.compile(exceptions_list) - - # Create a "fake" scope for the exception variable. - # See: https://docs.python.org/3/reference/compound_stmts.html#the-try-statement - with compiler.scope.create(ScopeLet) as scope: - if name: - scope.add(name, name) - body = compiler._compile_branch(body) - body += asty.Assign(expr, targets=[var], value=body.force_expr) - body += body.expr_as_stmt() - - return types + asty.ExceptHandler( - expr, type=types.expr, name=name, body=body.stmts or [asty.Pass(expr)] + x = (asty.TryStar if "except*" in except_syms_seen else asty.Try)( + expr, body=body, handlers=handlers, orelse=orelse, finalbody=finalbody ) + return handler_results + x + returnable # ------------------------------------------------ @@ -1390,8 +1454,10 @@ def compile_catch_expression(compiler, expr, var, exceptions, body): ) -@pattern_macro(["fn", "fn/a"], [maybe_annotated(lambda_list), many(FORM)]) -def compile_function_lambda(compiler, expr, root, params, body): +@pattern_macro(["fn", "fn/a"], + [maybe(type_params), maybe_annotated(lambda_list), many(FORM)]) +def compile_function_lambda(compiler, expr, root, tp, params, body): + is_async = root == "fn/a" params, returns = params posonly, args, rest, kwonly, kwargs = params has_annotations = returns is not None or any( @@ -1399,17 +1465,19 @@ def compile_function_lambda(compiler, expr, root, params, body): for param in (posonly or []) + args + kwonly + [rest, kwargs] ) args, ret = compile_lambda_list(compiler, params) - with compiler.scope.create(ScopeFn, args): + with compiler.local_state(), compiler.scope.create(ScopeFn, args, is_async) as scope: body = compiler._compile_branch(body) # Compile to lambda if we can - if not has_annotations and not body.stmts and root != "fn/a": + if not (has_annotations or tp or body.stmts or is_async): return ret + asty.Lambda(expr, args=args, body=body.force_expr) # Otherwise create a standard function - node = asty.AsyncFunctionDef if root == "fn/a" else asty.FunctionDef + node = asty.AsyncFunctionDef if is_async else asty.FunctionDef name = compiler.get_anon_var() - ret += compile_function_node(compiler, expr, node, [], name, args, returns, body) + ret += compile_function_node( + compiler, expr, node, [], tp, name, args, returns, body, scope + ) # return its name as the final expr return ret + Result(expr=ret.temp_variables[0]) @@ -1417,29 +1485,33 @@ def compile_function_lambda(compiler, expr, root, params, body): @pattern_macro( ["defn", "defn/a"], - [maybe(brackets(many(FORM))), maybe_annotated(SYM), lambda_list, many(FORM)], + [maybe(brackets(many(FORM))), maybe(type_params), maybe_annotated(SYM), lambda_list, many(FORM)], ) -def compile_function_def(compiler, expr, root, decorators, name, params, body): +def compile_function_def(compiler, expr, root, decorators, tp, name, params, body): + is_async = root == "defn/a" name, returns = name - node = asty.FunctionDef if root == "defn" else asty.AsyncFunctionDef + node = asty.AsyncFunctionDef if is_async else asty.FunctionDef decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else []) args, ret2 = compile_lambda_list(compiler, params) ret += ret2 name = mangle(compiler._nonconst(name)) compiler.scope.define(name) - with compiler.scope.create(ScopeFn, args): + with compiler.local_state(), compiler.scope.create(ScopeFn, args, is_async) as scope: body = compiler._compile_branch(body) return ret + compile_function_node( - compiler, expr, node, decorators, name, args, returns, body + compiler, expr, node, decorators, tp, name, args, returns, body, scope ) -def compile_function_node(compiler, expr, node, decorators, name, args, returns, body): +def compile_function_node(compiler, expr, node, decorators, tp, name, args, returns, body, scope): ret = Result() if body.expr: - body += asty.Return(body.expr, value=body.expr) + # implicitly return final expression, + # except for async generators + enode = asty.Expr if scope.is_async and scope.has_yield else asty.Return + body += enode(body.expr, value=body.expr) ret += node( expr, @@ -1448,6 +1520,7 @@ def compile_function_node(compiler, expr, node, decorators, name, args, returns, body=body.stmts or [asty.Pass(expr)], decorator_list=decorators, returns=compiler.compile(returns).force_expr if returns is not None else None, + **digest_type_params(compiler, tp), ) ast_name = asty.Name(expr, id=name, ctx=ast.Load()) @@ -1467,34 +1540,27 @@ def compile_function_node(compiler, expr, node, decorators, name, args, returns, ], ) def compile_macro_def(compiler, expr, root, name, params, body): - if "." in name: - raise compiler._syntax_error(name, "periods are not allowed in macro names") - - ret = Result() + compiler.compile( - Expression( - [ - Symbol("eval-and-compile"), - Expression( - [ - Expression( - [ - Symbol("hy.macros.macro"), - str(name), - ] - ), - Expression( - [ - Symbol("fn"), - List([Symbol("&compiler")] + list(expr[2])), - *body, - ] - ), - ] - ), - ] - ).replace(expr) - ) - + def E(*x): return Expression(x) + S = Symbol + + compiler.warn_on_core_shadow(name) + fn_def = E(S("fn"), List(expr[2]), *body).replace(expr) + if compiler.is_in_local_state(): + # We're in a local scope, so define the new macro locally. + state = compiler.local_state_stack[-1] + # Produce code that will set the macro to a local variable. + ret = compiler.compile(E( + S("setv"), + S(local_macro_name(name)), + fn_def).replace(expr)) + # Also evaluate the macro definition now, and put it in + # state['macros']. + state['macros'][mangle(name)] = compiler.eval(fn_def) + return ret + ret.expr_as_stmt() + # Otherwise, define the macro module-wide. + ret = compiler.compile(E(S("eval-and-compile"), E( + E(dotted("hy.macros.macro"), str(name)), + fn_def)).replace(expr)) return ret + ret.expr_as_stmt() @@ -1608,6 +1674,8 @@ def compile_return(compiler, expr, root, arg): @pattern_macro("yield", [maybe(FORM)]) def compile_yield_expression(compiler, expr, root, arg): + if is_inside_function_scope(compiler.scope): + nearest_python_scope(compiler.scope).has_yield = True ret = Result() if arg is not None: ret += compiler.compile(arg) @@ -1616,6 +1684,8 @@ def compile_yield_expression(compiler, expr, root, arg): @pattern_macro(["yield-from", "await"], [FORM]) def compile_yield_from_or_await_expression(compiler, expr, root, arg): + if root == "yield-from" and is_inside_function_scope(compiler.scope): + nearest_python_scope(compiler.scope).has_yield = True ret = Result() + compiler.compile(arg) node = asty.YieldFrom if root == "yield-from" else asty.Await return ret + node(expr, value=ret.force_expr) @@ -1630,11 +1700,12 @@ def compile_yield_from_or_await_expression(compiler, expr, root, arg): "defclass", [ maybe(brackets(many(FORM))), + maybe(type_params), SYM, maybe(brackets(many(FORM)) + maybe(STR) + many(FORM)), ], ) -def compile_class_expression(compiler, expr, root, decorators, name, rest): +def compile_class_expression(compiler, expr, root, decorators, tp, name, rest): base_list, docstring, body = rest or ([[]], None, []) decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else []) @@ -1651,7 +1722,7 @@ def compile_class_expression(compiler, expr, root, decorators, name, rest): name = mangle(compiler._nonconst(name)) compiler.scope.define(name) - with compiler.scope.create(ScopeFn): + with compiler.local_state(), compiler.scope.create(ScopeFn): e = compiler._compile_branch(body) bodyr += e + e.expr_as_stmt() @@ -1664,6 +1735,7 @@ def compile_class_expression(compiler, expr, root, decorators, name, rest): kwargs=None, bases=bases_expr, body=bodyr.stmts or [asty.Pass(expr)], + **digest_type_params(compiler, tp) ) @@ -1671,28 +1743,40 @@ def compile_class_expression(compiler, expr, root, decorators, name, rest): # * `import` and `require` # ------------------------------------------------ +module_name_pattern = SYM | pexpr( + some(lambda x: isinstance(x, Symbol) and not str(x[0]).strip(".")) + oneplus(SYM) +) + -def importlike(*name_types): - name = some(lambda x: isinstance(x, name_types) and "." not in x) +def module_name_str(x): return ( - keepsym("*") - | (keepsym(":as") + name) - | brackets(many(name + maybe(sym(":as") + name))) + ".".join(map(mangle, x[1][x[1][0] == Symbol("None") :])) + if isinstance(x, Expression) + else str(x) + if isinstance(x, Symbol) and not x.strip(".") + else mangle(x) ) +importlike = ( + keepsym("*") + | (keepsym(":as") + SYM) + | brackets(many(SYM + maybe(sym(":as") + SYM))) +) + + def assignment_shape(module, rest): prefix = "" assignments = "EXPORTS" if rest is None: # (import foo) - prefix = module + prefix = module_name_str(module) elif rest == Symbol("*"): # (import foo *) pass elif rest[0] == Keyword("as"): # (import foo :as bar) - prefix = rest[1] + prefix = mangle(rest[1]) else: # (import foo [bar baz :as MyBaz bing]) assignments = [(k, v or k) for k, v in rest[0]] @@ -1703,11 +1787,11 @@ def assignment_shape(module, rest): "require", [ many( - SYM + module_name_pattern + times( 0, 2, - (maybe(sym(":macros")) + importlike(Symbol)) + (maybe(sym(":macros")) + importlike) | (keepsym(":readers") + (keepsym("*") | brackets(many(SYM)))), ) ) @@ -1731,14 +1815,37 @@ def compile_require(compiler, expr, root, entries): readers = readers and readers[0] prefix, assignments = assignment_shape(module, rest) - ast_module = mangle(module) + module_name = module_name_str(module) + if isinstance(module, Expression) and module[1][0] == Symbol("None"): + # Prepend leading dots to `module_name`. + module_name = str(module[0]) + module_name # we don't want to import all macros as prefixed if we're specifically # importing readers but not macros # (require a-module :readers ["!"]) - if (rest or not readers) and require( - ast_module, compiler.module, assignments=assignments, prefix=prefix - ): + if (rest or not readers) and compiler.is_in_local_state(): + reqs = require( + module_name, + compiler.local_state_stack[-1]['macros'], + assignments = assignments, + prefix = prefix, + compiler = compiler) + ret += compiler.compile(Expression([ + Symbol("setv"), + List([Symbol(local_macro_name(m)) for m, _, _ in reqs]), + Expression([ + dotted("hy.macros.require_vals"), + String(module_name), + Dict(), + Keyword("assignments"), + List([(String(m), String(m)) for _, m, _ in reqs])])]).replace(expr)) + ret += ret.expr_as_stmt() + elif (rest or not readers) and require( + module_name, + compiler.module, + assignments = assignments, + prefix = prefix, + compiler = compiler): # Actually calling `require` is necessary for macro expansions # occurring during compilation. # The `require` we're creating in AST is the same as above, but used at @@ -1746,14 +1853,16 @@ def compile_require(compiler, expr, root, entries): ret += compiler.compile( Expression( [ - Symbol("hy.macros.require"), - String(ast_module), + dotted("hy.macros.require"), + String(module_name), Symbol("None"), + Keyword("target_module_name"), + String(compiler.module.__name__), Keyword("assignments"), ( String("EXPORTS") if assignments == "EXPORTS" - else [[String(k), String(v)] for k, v in assignments] + else List([List([String(k), String(v)]) for k, v in assignments]) ), Keyword("prefix"), String(prefix), @@ -1766,24 +1875,24 @@ def compile_require(compiler, expr, root, entries): reader_assignments = ( "ALL" if readers == Symbol("*") - else ["#" + reader for reader in readers[0]] + else [str(reader) for reader in readers[0]] ) - if require_reader(ast_module, compiler.module, reader_assignments): + if require_reader(module_name, compiler.module, reader_assignments): ret += compiler.compile( mkexpr( "do", mkexpr( - "hy.macros.require-reader", - String(ast_module), + dotted("hy.macros.require-reader"), + String(module_name), "None", [reader_assignments], ), mkexpr( "eval-when-compile", mkexpr( - "hy.macros.enable-readers", + dotted("hy.macros.enable-readers"), "None", - "hy.&reader", + mkexpr(dotted("hy.reader.HyReader.current-reader")), [reader_assignments], ), ), @@ -1793,31 +1902,25 @@ def compile_require(compiler, expr, root, entries): return ret -@pattern_macro("import", [many(SYM + maybe(importlike(Symbol)))]) +@pattern_macro("import", [many(module_name_pattern + maybe(importlike))]) def compile_import(compiler, expr, root, entries): ret = Result() for entry in entries: - assignments = "EXPORTS" - prefix = "" - module, _ = entry prefix, assignments = assignment_shape(*entry) - ast_module = mangle(module) - - module_name = ast_module.lstrip(".") - level = len(ast_module) - len(module_name) + module_name = module_name_str(module) if assignments == "EXPORTS" and prefix == "": node = asty.ImportFrom names = [asty.alias(module, name="*", asname=None)] elif assignments == "EXPORTS": - compiler.scope.define(mangle(prefix)) + compiler.scope.define(prefix) node = asty.Import names = [ asty.alias( module, - name=ast_module, - asname=mangle(prefix) if prefix != module else None, + name=module_name, + asname=prefix if prefix != module_name else None, ) ] else: @@ -1830,7 +1933,18 @@ def compile_import(compiler, expr, root, entries): module, name=mangle(k), asname=None if v == k else mangle(v) ) ) - ret += node(expr, module=module_name or None, names=names, level=level) + ret += node( + expr, + module=module_name if module_name and module_name.strip(".") else None, + names=names, + level=( + len(module[0]) + if isinstance(module, Expression) and module[1][0] == Symbol("None") + else len(module) + if isinstance(module, Symbol) and not module.strip(".") + else 0 + ), + ) return ret @@ -1885,8 +1999,28 @@ def compile_let(compiler, expr, root, bindings, body): return res + compiler.compile(mkexpr("do", *body).replace(expr)) +@pattern_macro(((3, 12), "deftype"), [maybe(type_params), SYM, FORM]) +def compile_deftype(compiler, expr, root, tp, name, value): + return asty.TypeAlias(expr, + name = asty.Name(name, id = mangle(name), ctx = ast.Store()), + value = compiler.compile(value).force_expr, + **digest_type_params(compiler, tp)) + + +@pattern_macro("pragma", [many(KEYWORD + FORM)]) +def compile_pragma(compiler, expr, root, kwargs): + for kw, value in kwargs: + if kw == Keyword("warn-on-core-shadow"): + compiler.local_state_stack[-1]['warn_on_core_shadow'] = ( + bool(compiler.eval(value))) + else: + raise compiler._syntax_error(kw, f"Unknown pragma `{kw}`. Perhaps it's implemented by a newer version of Hy.") + return Result() + + @pattern_macro( - "unquote unquote-splice unpack-mapping except finally else".split(), [many(FORM)] + "unquote unquote-splice unpack-mapping except except* finally else".split(), + [many(FORM)], ) def compile_placeholder(compiler, expr, root, body): raise ValueError(f"`{root}` is not allowed here") diff --git a/hy/core/util.hy b/hy/core/util.hy index 55d90061a..217b6366b 100644 --- a/hy/core/util.hy +++ b/hy/core/util.hy @@ -1,16 +1,12 @@ (import itertools) (import collections.abc [Iterable]) -(import hy.models [Keyword Symbol] - hy.reader [mangle unmangle] - hy.compiler [HyASTCompiler calling-module]) +(import hy.compiler [HyASTCompiler calling-module]) (defn disassemble [tree [codegen False]] "Return the python AST for a quoted Hy `tree` as a string. If the second argument `codegen` is true, generate python code instead. - .. versionadded:: 0.10.0 - Dump the Python AST for given Hy *tree* to standard output. If *codegen* is ``True``, the function prints Python code instead. @@ -41,13 +37,11 @@ (setv _gensym_counter 0) (setv _gensym_lock (Lock)) -(defn gensym [[g "G"]] +(defn gensym [[g ""]] #[[Generate a symbol with a unique name. The argument will be included in the - generated symbol, as an aid to debugging. Typically one calls ``hy.gensym`` + generated symbol name, as an aid to debugging. Typically one calls ``hy.gensym`` without an argument. - .. versionadded:: 0.9.12 - .. seealso:: Section :ref:`using-gensym` @@ -69,14 +63,18 @@ 4) (print (selfadd (f)))]] - (setv new_symbol None) - (global _gensym_counter) - (global _gensym_lock) (.acquire _gensym_lock) - (try (do (setv _gensym_counter (+ _gensym_counter 1)) - (setv new_symbol (Symbol (.format "_{}\uffff{}" g _gensym_counter)))) - (finally (.release _gensym_lock))) - new_symbol) + (try + (global _gensym_counter) + (+= _gensym_counter 1) + (setv n _gensym_counter) + (finally (.release _gensym_lock))) + (setv g (hy.mangle (.format "_hy_gensym_{}_{}" g n))) + (hy.models.Symbol (if (.startswith g "_hyx_") + ; Remove the mangle prefix, if it's there, so the result always + ; starts with our reserved prefix `_hy_`. + (+ "_" (cut g (len "_hyx_") None)) + g))) (defn _calling-module-name [[n 1]] "Get the name of the module calling `n` levels up the stack from the @@ -86,38 +84,42 @@ (setv f (get (.stack inspect) (+ n 1) 0)) (get f.f_globals "__name__")) -(defn macroexpand [form [result-ok False]] - "Return the full macro expansion of `form`. +(defn _macroexpand [model module macros #** kwargs] + (if (and (isinstance model hy.models.Expression) model) + (hy.macros.macroexpand + :tree model + :module module + :compiler (HyASTCompiler module :extra-macros macros) + :result-ok False + #** kwargs) + model)) - .. versionadded:: 0.10.0 +(defn macroexpand [model [module None] [macros None]] + "As :hy:func:`hy.macroexpand-1`, but the expansion process is repeated until it has no effect. :: - Examples: - :: + (defmacro m [x] + (and (int x) `(m ~(- x 1)))) + (print (hy.repr (hy.macroexpand-1 '(m 5)))) + ; => '(m 4) + (print (hy.repr (hy.macroexpand '(m 5)))) + ; => '0 - => (require hyrule [->]) - => (hy.macroexpand '(-> (a b) (x y))) - '(x (a b) y) - => (hy.macroexpand '(-> (a b) (-> (c d) (e f)))) - '(e (c (a b) d) f) - " - (import hy.macros) - (setv module (calling-module)) - (hy.macros.macroexpand form module (HyASTCompiler module) :result-ok result-ok)) + Note that in general, macro calls in the arguments of the expression still won't expanded. To expand these, too, try Hyrule's :hy:func:`macroexpand-all `." + (_macroexpand model (or module (calling-module)) macros)) -(defn macroexpand-1 [form] - "Return the single step macro expansion of `form`. +(defn macroexpand-1 [model [module None] [macros None]] + "Check if ``model`` is an :class:`Expression ` specifying a macro call. If so, expand the macro and return the expansion; otherwise, return ``model`` unchanged. :: - .. versionadded:: 0.10.0 + (defmacro m [x] + `(do ~x ~x ~x)) + (print (hy.repr (hy.macroexpand-1 '(m (+= n 1))))) + ; => '(do (+= n 1) (+= n 1) (+= n 1)) - Examples: - :: + An exceptional case is if the macro is a core macro that returns one of Hy's internal compiler result objects instead of a real model. Then, you just get the original back, as if the macro hadn't been expanded. - => (require hyrule [->]) - => (hy.macroexpand-1 '(-> (a b) (-> (c d) (e f)))) - '(-> (a b) (c d) (e f)) - " - (import hy.macros) - (setv module (calling-module)) - (hy.macros.macroexpand-1 form module (HyASTCompiler module))) + The optional arguments ``module`` and ``macros`` can be provided to control where macros are looked up, as with :hy:func:`hy.eval`. + + See also :hy:func:`hy.macroexpand`." + (_macroexpand model (or module (calling-module)) macros :once True)) (setv __all__ []) diff --git a/hy/errors.py b/hy/errors.py index 084effaf5..3bba6f1f3 100644 --- a/hy/errors.py +++ b/hy/errors.py @@ -1,18 +1,14 @@ import os -import pkgutil import re import sys import traceback +import importlib.util from contextlib import contextmanager -from functools import reduce - -from colorama import Fore from hy import _initialize_env_var from hy._compat import PYPY -_hy_filter_internal_errors = _initialize_env_var("HY_FILTER_INTERNAL_ERRORS", True) -COLORED = _initialize_env_var("HY_COLORED_ERRORS", False) +_hy_show_internal_errors = _initialize_env_var("HY_SHOW_INTERNAL_ERRORS", False) class HyError(Exception): @@ -135,20 +131,11 @@ def __str__(self): output[arrow_idx].rstrip("\n"), "-" * (self.arrow_offset - 1) ) - if COLORED: - output[msg_idx:] = [Fore.YELLOW + o + Fore.RESET for o in output[msg_idx:]] - if arrow_idx: - output[arrow_idx] = Fore.GREEN + output[arrow_idx] + Fore.RESET - for idx, line in enumerate(output[::msg_idx]): - if line.strip().startswith('File "{}", line'.format(self.filename)): - output[idx] = Fore.RED + line + Fore.RESET - # This resulting string will come after a ":" prompt, so # put it down a line. output.insert(0, "\n") - # Avoid "...expected str instance, ColoredString found" - return reduce(lambda x, y: x + y, output) + return "".join(output) class HyCompileError(HyInternalError): @@ -159,10 +146,6 @@ class HyTypeError(HyLanguageError, TypeError): """TypeError occurring during the normal use of Hy.""" -class HyNameError(HyLanguageError, NameError): - """NameError occurring during the normal use of Hy.""" - - class HyRequireError(HyLanguageError): """Errors arising during the use of `require` @@ -188,12 +171,6 @@ class HyEvalError(HyLanguageError): """ -class HyIOError(HyInternalError, IOError): - """Subclass used to distinguish between IOErrors raised by Hy itself as - opposed to Hy programs. - """ - - class HySyntaxError(HyLanguageError, SyntaxError): """Error during the Lexing of a Hython expression.""" @@ -209,7 +186,11 @@ class HyWrapperError(HyError, TypeError): def _module_filter_name(module_name): try: - compiler_loader = pkgutil.get_loader(module_name) + spec = importlib.util.find_spec(module_name) + if not spec: + return None + + compiler_loader = spec.loader if not compiler_loader: return None @@ -237,6 +218,7 @@ def _module_filter_name(module_name): "hy.compiler", "hy.reader", "hy.cmdline", + "hy.repl", "hy.reader.parser", "hy.importer", "hy._compat", @@ -272,6 +254,7 @@ def hy_exc_filter(exc_type, exc_value, exc_traceback): if not ( frame[0].replace(".pyc", ".py") in _tb_hidden_modules or os.path.dirname(frame[0]) in _tb_hidden_modules + or os.path.basename(os.path.dirname(frame[0])) == "hy.exe" ): new_tb += [frame] @@ -287,11 +270,11 @@ def hy_exc_filter(exc_type, exc_value, exc_traceback): def hy_exc_handler(exc_type, exc_value, exc_traceback): """A `sys.excepthook` handler that uses `hy_exc_filter` to - remove internal Hy frames from a traceback print-out. + remove internal Hy frames from a traceback print-out, so long + as `_hy_show_internal_errors` is false. """ - if os.environ.get("HY_DEBUG", False): + if _hy_show_internal_errors: return sys.__excepthook__(exc_type, exc_value, exc_traceback) - try: output = hy_exc_filter(exc_type, exc_value, exc_traceback) sys.stderr.write(output) @@ -306,14 +289,10 @@ def filtered_hy_exceptions(): from tracebacks. Filtering can be controlled by the variable - `hy.errors._hy_filter_internal_errors` and environment variable - `HY_FILTER_INTERNAL_ERRORS`. + `hy.errors._hy_show_internal_errors` and environment variable + `HY_SHOW_INTERNAL_ERRORS`. """ - global _hy_filter_internal_errors - if _hy_filter_internal_errors: - current_hook = sys.excepthook - sys.excepthook = hy_exc_handler - yield - sys.excepthook = current_hook - else: - yield + current_hook = sys.excepthook + sys.excepthook = hy_exc_handler + yield + sys.excepthook = current_hook diff --git a/hy/importer.py b/hy/importer.py index 9579f168e..554281e14 100644 --- a/hy/importer.py +++ b/hy/importer.py @@ -5,12 +5,13 @@ import pkgutil import sys import types +import zipimport from contextlib import contextmanager from functools import partial import hy from hy.compiler import hy_compile -from hy.reader import read_many +from hy.reader import read_many, HyReader @contextmanager @@ -107,10 +108,8 @@ def _get_code_from_file(run_name, fname=None, hy_src_check=lambda x: x.endswith( def _could_be_hy_src(filename): return os.path.isfile(filename) and ( - filename.endswith(".hy") - or not any( - filename.endswith(ext) for ext in importlib.machinery.SOURCE_SUFFIXES[1:] - ) + os.path.splitext(filename)[1] + not in set(importlib.machinery.SOURCE_SUFFIXES) - {".hy"} ) @@ -119,7 +118,7 @@ def _hy_source_to_code(self, data, path, _optimize=-1): if os.environ.get("HY_MESSAGE_WHEN_COMPILING"): print("Compiling", path, file=sys.stderr) source = data.decode("utf-8") - hy_tree = read_many(source, filename=path, skip_shebang=True) + hy_tree = read_many(source, filename=path, skip_shebang=True, reader=HyReader()) with loader_module_obj(self) as module: data = hy_compile(hy_tree, module) @@ -128,6 +127,29 @@ def _hy_source_to_code(self, data, path, _optimize=-1): importlib.machinery.SourceFileLoader.source_to_code = _hy_source_to_code + +if (".hy", False, False) not in zipimport._zip_searchorder: + zipimport._zip_searchorder += ((".hy", False, False),) + _py_compile_source = zipimport._compile_source + + def _hy_compile_source(pathname, source): + if not pathname.endswith(".hy"): + return _py_compile_source(pathname, source) + mname = f"" + sys.modules[mname] = types.ModuleType(mname) + return compile( + hy_compile( + read_many(source.decode("UTF-8"), filename=pathname, skip_shebang=True, reader=HyReader()), + sys.modules[mname], + ), + pathname, + "exec", + dont_inherit=True, + ) + + zipimport._compile_source = _hy_compile_source + + # This is actually needed; otherwise, pre-created finders assigned to the # current dir (i.e. `''`) in `sys.path` will not catch absolute imports of # directory-local modules! @@ -136,11 +158,6 @@ def _hy_source_to_code(self, data, path, _optimize=-1): # Do this one just in case? importlib.invalidate_caches() -# These aren't truly cross-compliant. -# They're useful for testing, though. -class HyImporter(importlib.machinery.FileFinder): - pass - class HyLoader(importlib.machinery.SourceFileLoader): pass @@ -160,14 +177,6 @@ class HyLoader(importlib.machinery.SourceFileLoader): runpy._get_code_from_file = _get_code_from_file -def _import_from_path(name, path): - """A helper function that imports a module from the given path.""" - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - def _inject_builtins(): """Inject the Hy core macros into Python's builtins if necessary""" if hasattr(builtins, "__hy_injected__"): diff --git a/hy/macros.py b/hy/macros.py index 91f726593..0bbe87a56 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -2,16 +2,15 @@ import importlib import inspect import os -import pkgutil +import re import sys import traceback -import warnings from ast import AST from funcparserlib.parser import NoParseError import hy.compiler -from hy._compat import code_replace +from hy._compat import PY3_11 from hy.errors import ( HyLanguageError, HyMacroExpansionError, @@ -20,7 +19,8 @@ ) from hy.model_patterns import whole from hy.models import Expression, Symbol, as_model, is_unpack, replace_hy_obj -from hy.reader import mangle, unmangle +from hy.reader import mangle +from hy.reader.mangling import slashes2dots EXTRA_MACROS = ["hy.core.result_macros", "hy.core.macros"] @@ -32,7 +32,7 @@ def macro(name): def reader_macro(name, fn): fn = rename_function(fn, name) - inspect.getmodule(fn).__dict__.setdefault("__reader_macros__", {})[name] = fn + fn.__globals__.setdefault("_hy_reader_macros", {})[name] = fn def pattern_macro(names, pattern, shadow=None): @@ -43,35 +43,34 @@ def pattern_macro(names, pattern, shadow=None): def dec(fn): def wrapper_maker(name): - def wrapper(hy_compiler, *args): + def wrapper(_hy_compiler, *args): if shadow and any(is_unpack("iterable", x) for x in args): # Try a shadow function call with this name instead. - return Expression([Symbol("hy.pyops." + name), *args]).replace( - hy_compiler.this - ) + return Expression( + [Expression(map(Symbol, [".", "hy", "pyops", name])), *args] + ).replace(_hy_compiler.this) - expr = hy_compiler.this - root = unmangle(expr[0]) + expr = _hy_compiler.this if py_version_required and sys.version_info < py_version_required: - raise hy_compiler._syntax_error( + raise _hy_compiler._syntax_error( expr, "`{}` requires Python {} or later".format( - root, ".".join(map(str, py_version_required)) + name, ".".join(map(str, py_version_required)) ), ) try: parse_tree = pattern.parse(args) except NoParseError as e: - raise hy_compiler._syntax_error( + raise _hy_compiler._syntax_error( expr[min(e.state.pos + 1, len(expr) - 1)], "parse error for pattern macro '{}': {}".format( - root, e.msg.replace("", "end of form") + name, e.msg.replace("end of input", "end of macro call") ), ) - return fn(hy_compiler, expr, root, *parse_tree) + return fn(_hy_compiler, expr, name, *parse_tree) return wrapper @@ -85,19 +84,7 @@ def wrapper(hy_compiler, *args): def install_macro(name, fn, module_of): name = mangle(name) fn = rename_function(fn, name) - calling_module = inspect.getmodule(module_of) - macros_obj = calling_module.__dict__.setdefault("__macros__", {}) - if name in getattr(builtins, "__macros__", {}): - warnings.warn( - ( - f"{name} already refers to: `{name}` in module: `builtins`," - f" being replaced by: `{calling_module.__name__}.{name}`" - ), - RuntimeWarning, - stacklevel=3, - ) - - macros_obj[name] = fn + module_of.__globals__.setdefault("_hy_macros", {})[name] = fn return fn @@ -109,31 +96,23 @@ def _same_modules(source_module, target_module): if not (source_module or target_module): return False - if target_module == source_module: + if target_module is source_module: return True - def _get_filename(module): - filename = None - try: - if not inspect.ismodule(module): - loader = pkgutil.get_loader(module) - if isinstance(loader, importlib.machinery.SourceFileLoader): - filename = loader.get_filename() - else: - filename = inspect.getfile(module) - except (TypeError, ImportError): - pass - - return filename + def get_filename(module): + if inspect.ismodule(module): + return inspect.getfile(module) + elif ( + (spec := importlib.util.find_spec(module)) and + isinstance(spec.loader, importlib.machinery.SourceFileLoader)): + return spec.loader.get_filename() - source_filename = _get_filename(source_module) - target_filename = _get_filename(target_module) - - return ( - source_filename - and target_filename - and os.path.samefile(source_filename, target_filename) - ) + try: + return os.path.samefile( + get_filename(source_module), + get_filename(target_module)) + except (ValueError, TypeError, ImportError, FileNotFoundError): + return False def derive_target_module(target_module, parent_frame): @@ -178,15 +157,15 @@ def require_reader(source_module, target_module, assignments): if not inspect.ismodule(source_module): source_module = import_module_from_string(source_module, target_module) - source_macros = source_module.__dict__.setdefault("__reader_macros__", {}) - target_macros = target_namespace.setdefault("__reader_macros__", {}) + source_macros = source_module.__dict__.setdefault("_hy_reader_macros", {}) + target_macros = target_namespace.setdefault("_hy_reader_macros", {}) assignments = ( - source_macros.keys() if assignments == "ALL" else map(mangle, assignments) + source_macros.keys() if assignments == "ALL" else assignments ) for name in assignments: - if name in source_module.__reader_macros__: + if name in source_module._hy_reader_macros: target_macros[name] = source_macros[name] else: raise HyRequireError(f"Could not require name {name} from {source_module}") @@ -197,79 +176,74 @@ def require_reader(source_module, target_module, assignments): def enable_readers(module, reader, names): _, namespace = derive_target_module(module, inspect.stack()[1][0]) names = ( - namespace["__reader_macros__"].keys() if names == "ALL" else map(mangle, names) + namespace["_hy_reader_macros"].keys() if names == "ALL" else names ) for name in names: - if name not in namespace["__reader_macros__"]: + if name not in namespace["_hy_reader_macros"]: raise NameError(f"reader {name} is not defined") - reader.reader_table[name] = namespace["__reader_macros__"][name] - + reader.reader_macros[name] = namespace["_hy_reader_macros"][name] -def require(source_module, target_module, assignments, prefix=""): - """Load macros from one module into the namespace of another. - This function is called from the macro also named `require`. +def require(source_module, target, assignments, prefix="", target_module_name=None, compiler=None): + """Load macros from a module. Return a list of (new name, source + name, macro object) tuples, including only macros that were + actually transferred. - Args: - source_module (Union[str, ModuleType]): The module from which macros are - to be imported. - target_module (Optional[Union[str, ModuleType]]): The module into which the - macros will be loaded. If `None`, then the caller's namespace. - The latter is useful during evaluation of generated AST/bytecode. - assignments (Union[str, typing.Sequence[str]]): The string "ALL", the string - "EXPORTS", or a list of macro name and alias pairs. - prefix (str): If nonempty, its value is prepended to the name of each imported macro. - This allows one to emulate namespaced macros, like "mymacromodule.mymacro", - which looks like an attribute of a module. Defaults to "" + - `target` can be a a string (naming a module), a module object, + a dictionary, or `None` (meaning the calling module). + - `assignments` can be "ALL", "EXPORTS", or a list of (macro + name, alias) pairs.""" - Returns: - bool: Whether or not macros were actually transferred. - """ - target_module, target_namespace = derive_target_module( - target_module, inspect.stack()[1][0] - ) - - # Let's do a quick check to make sure the source module isn't actually - # the module being compiled (e.g. when `runpy` executes a module's code - # in `__main__`). - # We use the module's underlying filename for this (when they exist), since - # it's the most "fixed" attribute. - if _same_modules(source_module, target_module): - return False + if type(target) is dict: + target_module = None + else: + target_module, target_namespace = derive_target_module( + target, inspect.stack()[1][0] + ) + # Let's do a quick check to make sure the source module isn't actually + # the module being compiled (e.g. when `runpy` executes a module's code + # in `__main__`). + # We use the module's underlying filename for this (when they exist), since + # it's the most "fixed" attribute. + if _same_modules(source_module, target_module): + return [] if not inspect.ismodule(source_module): - source_module = import_module_from_string(source_module, target_module) + source_module = import_module_from_string(source_module, + target_module_name or target_module or '') - source_macros = source_module.__dict__.setdefault("__macros__", {}) + source_macros = source_module.__dict__.setdefault("_hy_macros", {}) source_exports = getattr( source_module, "_hy_export_macros", [k for k in source_macros.keys() if not k.startswith("_")], ) - if not source_module.__macros__: + if not source_module._hy_macros: if assignments in ("ALL", "EXPORTS"): - return False + return [] + out = [] for name, alias in assignments: try: - require( + out.extend(require( f"{source_module.__name__}.{mangle(name)}", - target_module, + target_module or target, "ALL", prefix=alias, - ) + )) except HyRequireError as e: raise HyRequireError( f"Cannot import name '{name}'" f" from '{source_module.__name__}'" f" ({source_module.__file__})" ) - return True + return out - target_macros = target_namespace.setdefault("__macros__", {}) + target_macros = target_namespace.setdefault("_hy_macros", {}) if target_module else target if prefix: prefix += "." + out = [] for name, alias in ( assignments @@ -281,19 +255,30 @@ def require(source_module, target_module, assignments, prefix=""): ) ): _name = mangle(name) - alias = mangle( - "#" + prefix + unmangle(alias)[1:] - if unmangle(alias).startswith("#") - else prefix + alias - ) - if _name in source_module.__macros__: + if compiler: + compiler.warn_on_core_shadow(prefix + alias) + alias = mangle(prefix + alias) + if _name in source_module._hy_macros: target_macros[alias] = source_macros[_name] + out.append((alias, _name, source_macros[_name])) else: raise HyRequireError( "Could not require name {} from {}".format(_name, source_module) ) - return True + return out + + +def require_vals(*args, **kwargs): + return [value for _, _, value in require(*args, **kwargs)] + + +def local_macro_name(original): + return '_hy_local_macro__' + (mangle(original) + # We have to do a bit of extra mangling beyond `mangle` itself to + # handle names with periods. + .replace('D', 'DN') + .replace('.', 'DD')) def load_macros(module): @@ -302,19 +287,19 @@ def load_macros(module): It is an error to call this on any module in `hy.core`. """ builtin_macros = EXTRA_MACROS - module.__macros__ = {} - module.__reader_macros__ = {} + module._hy_macros = {} + module._hy_reader_macros = {} for builtin_mod_name in builtin_macros: builtin_mod = importlib.import_module(builtin_mod_name) # This may overwrite macros in the module. - if hasattr(builtin_mod, "__macros__"): - module.__macros__.update(getattr(builtin_mod, "__macros__", {})) + if hasattr(builtin_mod, "_hy_macros"): + module._hy_macros.update(getattr(builtin_mod, "_hy_macros", {})) - if hasattr(builtin_mod, "__reader_macros__"): - module.__reader_macros__.update( - getattr(builtin_mod, "__reader_macros__", {}) + if hasattr(builtin_mod, "_hy_reader_macros"): + module._hy_reader_macros.update( + getattr(builtin_mod, "_hy_reader_macros", {}) ) @@ -357,33 +342,13 @@ def __exit__(self, exc_type, exc_value, exc_traceback): def macroexpand(tree, module, compiler=None, once=False, result_ok=True): - """Expand the toplevel macros for the given Hy AST tree. - - Load the macros from the given `module`, then expand the (top-level) macros - in `tree` until we no longer can. - - `Expression` resulting from macro expansions are assigned the module in - which the macro function is defined (determined using `inspect.getmodule`). - If the resulting `Expression` is itself macro expanded, then the namespace - of the assigned module is checked first for a macro corresponding to the - expression's head/car symbol. If the head/car symbol of such a `Expression` - is not found among the macros of its assigned module's namespace, the - outer-most namespace--e.g. the one given by the `module` parameter--is used - as a fallback. - - Args: - tree (Union[Object, list]): Hy AST tree. - module (Union[str, ModuleType]): Module used to determine the local - namespace for macros. - compiler (Optional[HyASTCompiler] ): The compiler object passed to - expanded macros. Defaults to None - once (bool): Only expand the first macro in `tree`. Defaults to False - result_ok (bool): Whether or not it's okay to return a compiler `Result` instance. - Defaults to True. - - Returns: - Union[Object, Result]: A mutated tree with macros expanded. - """ + '''If `tree` isn't an `Expression` that might be a macro call, + return it unchanged. Otherwise, try to expand it. Do this + repeatedly unless `once` is true. Call `as_model` after each + expansion. If the return value is a compiler `Result` object, and + `result_ok` is false, return the previous value. Otherwise, return + the final expansion.''' + if not inspect.ismodule(module): module = importlib.import_module(module) @@ -392,54 +357,79 @@ def macroexpand(tree, module, compiler=None, once=False, result_ok=True): while isinstance(tree, Expression) and tree: fn = tree[0] - if fn in ("quote", "quasiquote") or not isinstance(fn, Symbol): + if isinstance(fn, Expression) and fn and fn[0] == Symbol("."): + fn = ".".join(map(mangle, fn[1:])) + elif isinstance(fn, Symbol): + fn = mangle(fn) + else: break - fn = mangle(fn) - expr_modules = ([] if not hasattr(tree, "module") else [tree.module]) + [module] - expr_modules.append(builtins) - - # Choose the first namespace with the macro. - m = next( - ( - mod.__macros__[fn] - for mod in expr_modules - if fn in getattr(mod, "__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: compiler.this = tree - obj = m(compiler, *tree[1:]) + obj = m( + # If the macro's first parameter is named + # `_hy_compiler`, pass in the current compiler object + # in its place. + *([compiler] + if m.__code__.co_varnames[:1] == ('_hy_compiler',) + else []), + *map(as_model, tree[1:])) if isinstance(obj, (hy.compiler.Result, AST)): return obj if result_ok else tree - if isinstance(obj, Expression): - obj.module = inspect.getmodule(m) - tree = replace_hy_obj(obj, tree) if once: break - tree = as_model(tree) return tree -def macroexpand_1(tree, module, compiler=None): - """Expand the toplevel macro from `tree` once, in the context of - `compiler`.""" - return macroexpand(tree, module, compiler, once=True) - - def rename_function(f, new_name): """Create a copy of a function, but with a new name.""" f = type(f)( - code_replace(f.__code__, co_name=new_name), + f.__code__.replace( + co_name=new_name, + **( + { + "co_qualname": re.sub( + r"\.[^.+]\Z", "." + new_name, f.__code__.co_qualname + ) + if "." in f.__code__.co_qualname + else new_name + } + if PY3_11 + else {} + ), + ), f.__globals__, str(new_name), f.__defaults__, diff --git a/hy/model_patterns.py b/hy/model_patterns.py index 1257e912f..1d7033958 100644 --- a/hy/model_patterns.py +++ b/hy/model_patterns.py @@ -31,11 +31,11 @@ Tuple, ) -FORM = some(lambda _: True) -SYM = some(lambda x: isinstance(x, Symbol)) -KEYWORD = some(lambda x: isinstance(x, Keyword)) -STR = some(lambda x: isinstance(x, String)) # matches literal strings only! -LITERAL = some(lambda x: isinstance(x, (String, Integer, Float, Complex, Bytes))) +FORM = some(lambda _: True).named('form') +SYM = some(lambda x: isinstance(x, Symbol)).named('Symbol') +KEYWORD = some(lambda x: isinstance(x, Keyword)).named('Keyword') +STR = some(lambda x: isinstance(x, String)).named('String') # matches literal strings only! +LITERAL = some(lambda x: isinstance(x, (String, Integer, Float, Complex, Bytes))).named('literal') def sym(wanted): @@ -49,9 +49,10 @@ def keepsym(wanted): def _sym(wanted, f=lambda x: x): + name = '`' + wanted + '`' if wanted.startswith(":"): - return f(a(Keyword(wanted[1:]))) - return f(some(lambda x: x == Symbol(wanted))) + return f(a(Keyword(wanted[1:]))).named(name) + return f(some(lambda x: x == Symbol(wanted))).named(name) def whole(parsers): @@ -64,29 +65,23 @@ def whole(parsers): return reduce(add, parsers) + skip(finished) -def _grouped(group_type, parsers): - return some(lambda x: isinstance(x, group_type)) >> ( - lambda x: group_type(whole(parsers).parse(x)).replace(x, recursive=False) +def _grouped(group_type, syntax_example, name, parsers): + return ( + some(lambda x: isinstance(x, group_type)).named(name or + f'{group_type.__name__} (i.e., `{syntax_example}`)') >> + (lambda x: group_type(whole(parsers).parse(x)).replace(x, recursive=False)) ) - - -def brackets(*parsers): +def brackets(*parsers, name = None): "Parse the given parsers inside square brackets." - return _grouped(List, parsers) - - -def in_tuple(*parsers): - return _grouped(Tuple, parsers) - - -def braces(*parsers): + return _grouped(List, '[ … ]', name, parsers) +def in_tuple(*parsers, name = None): + return _grouped(Tuple, '#( … )', name, parsers) +def braces(*parsers, name = None): "Parse the given parsers inside curly braces" - return _grouped(Dict, parsers) - - -def pexpr(*parsers): + return _grouped(Dict, '{ … }', name, parsers) +def pexpr(*parsers, name = None): "Parse the given parsers inside a parenthesized expression." - return _grouped(Expression, parsers) + return _grouped(Expression, '( … )', name, parsers) def dolike(head): @@ -103,13 +98,15 @@ def notpexpr(*disallowed_heads): ) -def unpack(kind): +def unpack(kind, content_type = None): "Parse an unpacking form, returning it unchanged." - return some( - lambda x: isinstance(x, Expression) - and len(x) > 0 - and x[0] == Symbol("unpack-" + kind) - ) + return some(lambda x: + isinstance(x, Expression) and + len(x) > 0 and + x[0] in [Symbol("unpack-" + tail) for tail in + (["iterable", "mapping"] if kind == "either" else [kind])] and + (content_type is None or + (len(x) == 2 and isinstance(x[1], content_type)))) def times(lo, hi, parser): diff --git a/hy/models.py b/hy/models.py index 2f468ca5e..23ccb6c56 100644 --- a/hy/models.py +++ b/hy/models.py @@ -4,13 +4,10 @@ from itertools import groupby from math import isinf, isnan -from colorama import Fore - from hy import _initialize_env_var from hy.errors import HyWrapperError PRETTY = True -COLORED = _initialize_env_var("HY_COLORED_AST_OBJECTS", False) @contextmanager @@ -27,18 +24,6 @@ def pretty(pretty=True): PRETTY = old -class _ColoredModel: - """ - Mixin that provides a helper function for models that have color. - """ - - def _colored(self, text): - if COLORED: - return self.color + text + Fore.RESET - else: - return text - - class Object: "An abstract base class for Hy models, which represent forms." @@ -49,7 +34,7 @@ class Object: `end_column` 3. """ - properties = ["module", "_start_line", "_end_line", "_start_column", "_end_column"] + properties = ["_start_line", "_end_line", "_start_column", "_end_column"] def replace(self, other, recursive=False): if isinstance(other, Object): @@ -218,7 +203,7 @@ class Symbol(Object, str): """ Represents a symbol. - Symbol objects behave like strings under operations like :hy:func:`get`, + Symbol objects behave like strings under operations like :hy:func:`get `, :func:`len`, and :class:`bool`; in particular, ``(bool (hy.models.Symbol "False"))`` is true. Use :hy:func:`hy.eval` to evaluate a symbol. """ @@ -227,9 +212,9 @@ def __new__(cls, s, from_parser=False): if not from_parser: # Check that the symbol is syntactically legal. # import here to prevent circular imports. - from hy.reader.hy_reader import symbol_like + from hy.reader.hy_reader import as_identifier - sym = symbol_like(s) + sym = as_identifier(s) if not isinstance(sym, Symbol): raise ValueError(f"Syntactically illegal symbol: {s!r}") return sym @@ -237,7 +222,7 @@ def __new__(cls, s, from_parser=False): _wrappers[bool] = lambda x: Symbol("True") if x else Symbol("False") -_wrappers[type(None)] = lambda foo: Symbol("None") +_wrappers[type(None)] = lambda _: Symbol("None") class Keyword(Object): @@ -298,8 +283,8 @@ def __call__(self, data, default=_sentinel): :class:`hy.models.Keyword` objects). The optional second parameter is a default value; if provided, any - :class:`KeyError` from :hy:func:`get` will be caught, and the default returned - instead.""" + :class:`KeyError` from :hy:func:`get ` will be caught, + and the default returned instead.""" from hy.reader import mangle @@ -327,21 +312,19 @@ class Integer(Object, int): """ def __new__(cls, number, *args, **kwargs): - if isinstance(number, str): - number = strip_digit_separators(number) - bases = {"0o": 8, "0b": 2} - for leader, base in bases.items(): - if number.startswith(leader): - # We've got a string, known leader, set base. - number = int(number, base=base) - break - else: - # We've got a string, no known leader; base 10. - number = int(number, base=10) - else: - # We've got a non-string; convert straight. - number = int(number) - return super().__new__(cls, number) + return super().__new__( + cls, + int( + strip_digit_separators(number), + **( + {"base": 0} + if isinstance(number, str) and not number.isdigit() + # `not number.isdigit()` is necessary because `base = 0` + # fails on decimal integers starting with a leading 0. + else {} + ), + ), + ) _wrappers[int] = Integer @@ -378,7 +361,7 @@ def __new__(cls, real, imag=0, *args, **kwargs): if isinstance(real, str): value = super().__new__(cls, strip_digit_separators(real)) p1, _, p2 = real.lstrip("+-").replace("-", "+").partition("+") - check_inf_nan_cap(p1, value.imag if "j" in p1 else value.real) + check_inf_nan_cap(p1, value.imag if "j" in p1.lower() else value.real) if p2: check_inf_nan_cap(p2, value.imag) return value @@ -388,7 +371,7 @@ def __new__(cls, real, imag=0, *args, **kwargs): _wrappers[complex] = Complex -class Sequence(Object, tuple, _ColoredModel): +class Sequence(Object, tuple): """ An abstract base class for sequence-like forms. Sequence models can be operated on like tuples: you can iterate over them, index into them, and append them with ``+``, @@ -425,8 +408,6 @@ def __getitem__(self, item): return ret - color = None - def __repr__(self): return self._pretty_str() if PRETTY else super().__repr__() @@ -435,17 +416,12 @@ def __str__(self): def _pretty_str(self): with pretty(): - if self: - return self._colored( - "hy.models.{}{}\n {}{}".format( - self._colored(self.__class__.__name__), - self._colored("(["), - self._colored(",\n ").join(map(repr_indent, self)), - self._colored("])"), - ) - ) - else: - return self._colored(f"hy.models.{self.__class__.__name__}()") + return "hy.models.{}({})".format( + self.__class__.__name__, + "[\n {}]".format(",\n ".join(map(repr_indent, self))) + if self + else "" + ) class FComponent(Sequence): @@ -534,7 +510,7 @@ class List(Sequence): clauses. """ - color = Fore.CYAN + pass def recwrap(f): @@ -556,16 +532,14 @@ def lambda_to_return(l): _wrappers[list] = recwrap(List) -class Dict(Sequence, _ColoredModel): +class Dict(Sequence): """ Represents a literal :class:`dict`. ``keys``, ``values``, and ``items`` methods are provided, each returning a list, although this model type does none of the normalization of a real :class:`dict`. In the case of an odd number of child models, - ``keys`` returns the last child whereas ``values`` and ``items`` ignores it. + ``keys`` returns the last child whereas ``values`` and ``items`` ignore it. """ - color = Fore.GREEN - def _pretty_str(self): with pretty(): if self: @@ -573,21 +547,19 @@ def _pretty_str(self): for k, v in zip(self[::2], self[1::2]): k, v = repr_indent(k), repr_indent(v) pairs.append( - ("{0}{c}\n {1}\n " if "\n" in k + v else "{0}{c} {1}").format( - k, v, c=self._colored(",") + ("{0},\n {1}\n " if "\n" in k + v else "{0}, {1}").format( + k, v ) ) if len(self) % 2 == 1: pairs.append( - "{} {}\n".format(repr_indent(self[-1]), self._colored("# odd")) + "{} # odd\n".format(repr_indent(self[-1])) ) - return "{}\n {}{}".format( - self._colored("hy.models.Dict(["), - "{c}\n ".format(c=self._colored(",")).join(pairs), - self._colored("])"), + return "hy.models.Dict([\n {}])".format( + ",\n ".join(pairs), ) else: - return self._colored("hy.models.Dict()") + return "hy.models.Dict()" def keys(self): return list(self[0::2]) @@ -616,7 +588,7 @@ class Expression(Sequence): Represents a parenthesized Hy expression. """ - color = Fore.YELLOW + pass _wrappers[Expression] = recwrap(Expression) @@ -628,7 +600,7 @@ class Set(Sequence): and the order of elements. """ - color = Fore.RED + pass _wrappers[Set] = recwrap(Set) @@ -640,7 +612,7 @@ class Tuple(Sequence): Represents a literal :class:`tuple`. """ - color = Fore.BLUE + pass _wrappers[Tuple] = recwrap(Tuple) diff --git a/hy/pyops.hy b/hy/pyops.hy index b09ca8c9d..95941d13a 100644 --- a/hy/pyops.hy +++ b/hy/pyops.hy @@ -1,8 +1,16 @@ "Python provides various :ref:`binary and unary operators `. These are usually invoked in Hy using core macros of the same name: for example, ``(+ 1 2)`` calls the core macro named -``+``, which uses Python's addition operator. An exception to the names -being the same is that Python's ``==`` is called ``=`` in Hy. +``+``, which uses Python's addition operator. There are a few exceptions +to the names being the same: + +- ``==`` in Python is :hy:func:`= ` in Hy. +- ``~`` in Python is :hy:func:`bnot ` in Hy. +- ``is not`` in Python is :hy:func:`is-not ` in Hy. +- ``not in`` in Python is :hy:func:`not-in ` in Hy. + +For Python's subscription expressions (like ``x[2]``), Hy has two named +macros, :hy:func:`get ` and :hy:func:`cut `. By importing from the module ``hy.pyops`` (typically with a star import, as in ``(import hy.pyops *)``), you can also use these operators as @@ -13,8 +21,18 @@ macro instead of the function. The functions in ``hy.pyops`` have the same semantics as their macro equivalents, with one exception: functions can't short-circuit, so the -functions for the logical operators, such as ``and``, unconditionally -evaluate all arguments." +functions for operators such as ``and`` and ``!=`` unconditionally +evaluate all arguments. + +Hy also provides macros for :ref:`Python's augmented assignment +operators ` (but no equivalent functions, because Python +semantics don't allow for this). These macros require at least two +arguments even if the parent operator doesn't; for example, ``(-= x)`` +is an error even though ``(- x)`` is legal. On the other hand, +augmented-assignment macros extend to more than two arguments in an +analogous way as the parent operator, following the pattern ``(OP= x a b +c …)`` → ``(OP= x (OP a b c …))``. For example, ``(+= count n1 n2 n3)`` +is equivalent to ``(+= count (+ n1 n2 n3)).``" ;;;; Hy shadow functions @@ -66,7 +84,7 @@ evaluate all arguments." (defop * [#* args] ["multiplication" - :nullary "0" + :nullary "1" :unary "x"] (if (= (len args) 0) 1 @@ -132,12 +150,13 @@ evaluate all arguments." :n-ary None] (^ x y)) -(defop ~ [x] +(defop bnot [x] ["bitwise NOT" + :pyop "~" :unary "~x" :binary None :n-ary None] - (~ x)) + (bnot x)) (defn comp-op [op a1 a-rest] "Helper for shadow comparison operators" @@ -203,44 +222,64 @@ evaluate all arguments." (not x)) (defn get [coll key1 #* keys] - "Access item in `coll` indexed by `key1`, with optional `keys` nested-access. - - ``get`` is used to access single elements in collections. ``get`` takes at - least two parameters: the *data structure* and the *index* or *key* of the - item. It will then return the corresponding value from the collection. If - multiple *index* or *key* values are provided, they are used to access - successive elements in a nested structure. - - .. note:: ``get`` raises a KeyError if a dictionary is queried for a - non-existing key. - - .. note:: ``get`` raises an IndexError if a list or a tuple is queried for an - index that is out of bounds. - - Examples: - :: - - => (do - ... (setv animals {\"dog\" \"bark\" \"cat\" \"meow\"} - ... numbers #(\"zero\" \"one\" \"two\" \"three\") - ... nested [0 1 [\"a\" \"b\" \"c\"] 3 4]) - ... (print (get animals \"dog\")) - ... (print (get numbers 2)) - ... (print (get nested 2 1)) - bark - two - b - " + #[[``get`` compiles to one or more :ref:`subscription expressions `, + which select an element of a data structure. The first two arguments are the + collection object and a key; for example, ``(get person name)`` compiles to + ``person[name]``. Subsequent arguments indicate chained subscripts, so ``(get person + name "surname" 0)`` becomes ``person[name]["surname"][0]``. You can assign to a + ``get`` form, as in :: + + (setv real-estate {"price" 1,500,000}) + (setv (get real-estate "price") 0) + + but this doesn't work with the function version of ``get`` from ``hy.pyops``, due to + Python limitations on lvalues. + + If you're looking for the Hy equivalent of Python list slicing, as in ``foo[1:3]``, + note that this is just Python's syntactic sugar for ``foo[slice(1, 3)]``, and Hy + provides its own syntactic sugar for this with a different macro, :hy:func:`cut `. + + Note that ``.`` (:ref:`dot `) forms can also subscript. See also Hyrule's + :hy:func:`assoc ` to easily assign multiple elements of a + single collection.]] + (setv coll (get coll key1)) (for [k keys] (setv coll (get coll k))) coll) +(defn cut [coll / [arg1 'sentinel] [arg2 'sentinel] [arg3 'sentinel]] + #[[``cut`` compiles to a :ref:`slicing expression `, which selects multiple + elements of a sequential data structure. The first argument is the object to be + sliced. The remaining arguments are optional, and understood the same way as in a + Python slicing expression. :: + + (setv x "abcdef") + (cut x) ; => "abcdef" + (cut x 3) ; => "abc" + (cut x 3 5) ; => "de" + (cut x -3 None) ; => "def" + (cut x 0 None 2) ; => "ace" + + A call to the ``cut`` macro (but not its function version in ``hy.pyops``) is a valid + target for assignment (with :hy:func:`setv`, ``+=``, etc.) and for deletion (with + :hy:func:`del`).]] + + (cond + (= arg1 'sentinel) + (cut coll) + (= arg2 'sentinel) + (cut coll arg1) + (= arg3 'sentinel) + (cut coll arg1 arg2) + True + (cut coll arg1 arg2 arg3))) + (setv __all__ (list (map hy.mangle [ '+ '- '* '** '/ '// '% '@ - '<< '>> '& '| '^ '~ + '<< '>> '& '| '^ 'bnot '< '> '<= '>= '= '!= 'and 'or 'not 'is 'is-not 'in 'not-in - 'get]))) + 'get 'cut]))) diff --git a/hy/reader/__init__.py b/hy/reader/__init__.py index 54a856401..09147c8c4 100644 --- a/hy/reader/__init__.py +++ b/hy/reader/__init__.py @@ -30,18 +30,15 @@ def read_many(stream, filename="", reader=None, skip_shebang=False): if isinstance(stream, str): stream = StringIO(stream) pos = stream.tell() - if skip_shebang: - if stream.read(2) == "#!": - stream.readline() - pos = stream.tell() - else: - stream.seek(pos) source = stream.read() stream.seek(pos) - m = hy.models.Lazy((reader or HyReader()).parse(stream, filename)) + reader = reader or HyReader() + m = hy.models.Lazy(reader.parse( + stream, filename, skip_shebang)) m.source = source m.filename = filename + m.reader = reader return m @@ -57,5 +54,5 @@ def read(stream, filename=None, reader=None): except StopIteration: raise EOFError() else: - m.source, m.filename = it.source, it.filename + m.source, m.filename, m.reader = it.source, it.filename, it.reader return m diff --git a/hy/reader/hy_reader.py b/hy/reader/hy_reader.py index 0e80b2a3a..6e89e269b 100644 --- a/hy/reader/hy_reader.py +++ b/hy/reader/hy_reader.py @@ -1,5 +1,10 @@ "Character reader for parsing Hy source." +import codecs +import inspect +from contextlib import contextmanager, nullcontext +from itertools import islice + import hy from hy.models import ( Bytes, @@ -34,8 +39,8 @@ def mkexpr(root, *args): return Expression((sym(root) if isinstance(root, str) else root, *args)) -def symbol_like(ident, reader=None): - """Generate a Hy model from an identifier-like string. +def as_identifier(ident, reader=None): + """Generate a Hy model from an identifier. Also verifies the syntax of dot notation and validity of symbol names. @@ -66,22 +71,37 @@ def symbol_like(ident, reader=None): pass if "." in ident: - for chunk in ident.split("."): - if chunk and not isinstance(symbol_like(chunk, reader=reader), Symbol): - msg = ( - "Cannot access attribute on anything other" - " than a name (in order to get attributes of expressions," - " use `(. )` or `(. )`)" - ) - if reader is None: - raise ValueError(msg) - else: - raise LexException.from_reader(msg, reader) + if not ident.strip("."): + # It's all dots. Return it as a symbol. + return sym(ident) + + def err(msg): + raise ( + ValueError(msg) + if reader is None + else LexException.from_reader(msg, reader) + ) + + if ident.lstrip(".").find("..") > 0: + err( + "In a dotted identifier, multiple dots in a row are only allowed at the start" + ) + if ident.endswith("."): + err("A dotted identifier can't end with a dot") + head = "." * (len(ident) - len(ident.lstrip("."))) + args = [as_identifier(a, reader=reader) for a in ident.lstrip(".").split(".")] + if any(not isinstance(a, Symbol) for a in args): + err("The parts of a dotted identifier must be symbols") + return ( + mkexpr(sym("."), *args) + if head == "" + else mkexpr(head, Symbol("None"), *args) + ) if reader is None: if ( not ident - or ident[:1] == ":" + or ident[0] in ":#" or any(isnormalizedspace(c) for c in ident) or HyReader.NON_IDENT.intersection(ident) ): @@ -91,13 +111,53 @@ def symbol_like(ident, reader=None): class HyReader(Reader): - """A modular reader for Hy source.""" + """A modular reader for Hy source. + + When ``use_current_readers`` is true, initialize this reader + with all reader macros from the calling module.""" ### # Components necessary for Reader implementation ### - NON_IDENT = set("()[]{};\"'") + NON_IDENT = set("()[]{};\"'`~") + _current_reader = None + + def __init__(self, *, use_current_readers=False): + super().__init__() + + # move any reader macros declared using + # `reader_for("#...")` to the macro table + self.reader_macros = {} + for tag in list(self.reader_table.keys()): + if tag[0] == '#' and tag[1:]: + self.reader_macros[tag[1:]] = self.reader_table.pop(tag) + + if use_current_readers: + self.reader_macros.update( + inspect.stack()[1].frame.f_globals.get("_hy_reader_macros", {}) + ) + + @classmethod + def current_reader(cls, override=None, create=True): + return override or HyReader._current_reader or (cls() if create else None) + + @contextmanager + def as_current_reader(self): + old_reader = HyReader._current_reader + HyReader._current_reader = self + try: + yield + finally: + HyReader._current_reader = old_reader + + @classmethod + @contextmanager + def using_reader(cls, override=None, create=True): + reader = cls.current_reader(override, create) + with reader.as_current_reader() if reader else nullcontext(): + yield + def fill_pos(self, model, start): """Attach line/col information to a model. @@ -111,44 +171,43 @@ def fill_pos(self, model, start): """ model.start_line, model.start_column = start model.end_line, model.end_column = self.pos - return model + return model.replace(model) + # `replace` will recurse into submodels and set any model + # positions that are still unset the same way. def read_default(self, key): """Default reader handler when nothing in the table matches. - Try to read an identifier/symbol. If there's a double-quote immediately - following, then parse it as a string with the given prefix (e.g., - `r"..."`). Otherwise, parse it as a symbol-like. + Try to read an identifier. If there's a double-quote immediately + following, then instead parse it as a string with the given prefix (e.g., + `r"..."`). """ ident = key + self.read_ident() if self.peek_and_getc('"'): return self.prefixed_string('"', ident) - return symbol_like(ident, reader=self) + return as_identifier(ident, reader=self) - def parse(self, stream, filename=None): + def parse(self, stream, filename=None, skip_shebang=False): """Yields all `hy.models.Object`'s in `source` - Additionally exposes `self` as ``hy.&reader`` during read/compile time. - Args: source: Hy source to be parsed. filename (str | None): Filename to use for error messages. If `None` then previously set filename is used. + skip_shebang: + Whether to detect a skip a shebang line at the start. """ self._set_source(stream, filename) - rname = mangle("&reader") - old_reader = getattr(hy, rname, None) - setattr(hy, rname, self) - try: - yield from self.parse_forms_until("") - finally: - if old_reader is None: - delattr(hy, rname) - else: - setattr(hy, rname, old_reader) + if skip_shebang and "".join( + islice(self.peeking(eof_ok = True), len("#!"))) == "#!": + for c in self.chars(): + if c == "\n": + break + + yield from self.parse_forms_until("") ### # Reading forms @@ -170,23 +229,28 @@ def try_parse_one_form(self): fully parsing a form. LexException: If there is an error during form parsing. """ - try: - self.slurp_space() - c = self.getc() - start = self._pos - if not c: - raise PrematureEndOfInput.from_reader( - "Premature end of input while attempting to parse one form", self + with self.as_current_reader(): + try: + self.slurp_space() + c = self.getc() + start = self._pos + if not c: + raise PrematureEndOfInput.from_reader( + "Premature end of input while attempting to parse one form", self + ) + handler = self.reader_table.get(c) + model = handler(self, c) if handler else self.read_default(c) + if model is not None: + model = self.fill_pos(model, start) + model.reader = self + return model + return None + except LexException: + raise + except Exception as e: + raise LexException.from_reader( + str(e) or "Exception thrown attempting to parse one form", self ) - handler = self.reader_table.get(c) - model = handler(self, c) if handler else self.read_default(c) - return self.fill_pos(model, start) if model is not None else None - except LexException: - raise - except Exception as e: - raise LexException.from_reader( - str(e) or "Exception thrown attempting to parse one form", self - ) def parse_one_form(self): """Read from the stream until a form is parsed. @@ -283,26 +347,10 @@ def quote_closing(c): @reader_for("'", ("quote",)) @reader_for("`", ("quasiquote",)) def tag_as(root): - def _tag_as(self, _): - nc = self.peekc() - if ( - not nc - or isnormalizedspace(nc) - or self.reader_table.get(nc) == self.INVALID - ): - raise LexException.from_reader( - "Could not identify the next token.", self - ) - model = self.parse_one_form() - return mkexpr(root, model) - - return _tag_as + return lambda self, _: mkexpr(root, self.parse_one_form()) @reader_for("~") def unquote(self, key): - nc = self.peekc() - if not nc or isnormalizedspace(nc) or self.reader_table.get(nc) == self.INVALID: - return sym(key) return mkexpr( "unquote" + ("-splice" if self.peek_and_getc("@") else ""), self.parse_one_form(), @@ -330,37 +378,21 @@ def tag_dispatch(self, key): Reads a full identifier after the `#` and calls the corresponding handler (this allows, e.g., `#reads-multiple-forms foo bar baz`). - - Failing that, reads a single character after the `#` and immediately - calls the corresponding handler (this allows, e.g., `#*args` to parse - as `#*` followed by `args`). """ - if not self.peekc(): + if not self.peekc().strip(): raise PrematureEndOfInput.from_reader( "Premature end of input while attempting dispatch", self ) - if self.peek_and_getc("^"): - typ = self.parse_one_form() - target = self.parse_one_form() - return mkexpr("annotate", target, typ) - - tag = None # try dispatching tagged ident - ident = self.read_ident(just_peeking=True) - if ident and mangle(key + ident) in self.reader_table: - self.getn(len(ident)) - tag = mangle(key + ident) - # failing that, dispatch tag + single character - elif key + self.peekc() in self.reader_table: - tag = key + self.getc() - if tag: - tree = self.dispatch(tag) + ident = self.read_ident() or self.getc() + if ident in self.reader_macros: + tree = self.reader_macros[ident](self, ident) return as_model(tree) if tree is not None else None raise LexException.from_reader( - f"reader macro '{key + self.read_ident()}' is not defined", self + f"reader macro '{key + ident}' is not defined", self ) @reader_for("#_") @@ -370,18 +402,21 @@ def discard(self, _): return None @reader_for("#*") - def hash_star(self, _): + @reader_for("#**") + def hash_star(self, stars): """Unpacking forms `#*` and `#**`, corresponding to `*` and `**` in Python.""" - num_stars = 1 - while self.peek_and_getc("*"): - num_stars += 1 - if num_stars > 2: - raise LexException.from_reader("too many stars", self) return mkexpr( - "unpack-" + ("iterable", "mapping")[num_stars - 1], + "unpack-" + {"*": "iterable", "**": "mapping"}[stars], self.parse_one_form(), ) + @reader_for("#^") + def annotate(self, _): + """Annotate a symbol, usually with a type.""" + typ = self.parse_one_form() + target = self.parse_one_form() + return mkexpr("annotate", target, typ) + ### # Strings # (these are more complicated because f-strings @@ -428,7 +463,7 @@ def delim_closing(c): index = -1 return 0 - return self.read_string_until(delim_closing, None, is_fstring, brackets=delim) + return self.read_string_until(delim_closing, "r", is_fstring, brackets=delim) def read_string_until(self, closing, prefix, is_fstring, **kwargs): if is_fstring: @@ -439,6 +474,7 @@ def read_string_until(self, closing, prefix, is_fstring, **kwargs): def read_chars_until(self, closing, prefix, is_fstring): s = [] + in_named_escape = False for c in self.chars(): s.append(c) # check if c is closing @@ -447,19 +483,39 @@ def read_chars_until(self, closing, prefix, is_fstring): # string has ended s = s[:-n_closing_chars] break - # check if c is start of component - if is_fstring and c == "{" and s[-3:] != ["\\", "N", "{"]: - # check and handle "{{" - if self.peek_and_getc("{"): - s.append("{") - else: - # remove "{" from end of string component - s.pop() - break + if is_fstring: + # handle braces in f-strings + if c == "{": + if "r" not in prefix and s[-3:] == ["\\", "N", "{"]: + # ignore "\N{...}" + in_named_escape = True + elif not self.peek_and_getc("{"): + # start f-component if not "{{" + s.pop() + break + elif c == "}": + if in_named_escape: + in_named_escape = False + elif not self.peek_and_getc("}"): + raise SyntaxError("f-string: single '}' is not allowed") res = "".join(s).replace("\x0d\x0a", "\x0a").replace("\x0d", "\x0a") - if prefix is not None: - res = eval(f'{prefix}"""{res}"""') + if "b" in prefix: + try: + res = res.encode('ascii') + except UnicodeEncodeError: + raise SyntaxError("bytes can only contain ASCII literal characters") + + if "r" not in prefix: + # perform string escapes + if "b" in prefix: + res = codecs.escape_decode(res)[0] + else: + # formula taken from https://stackoverflow.com/a/57192592 + # encode first to ISO-8859-1 ("Latin 1") due to a Python bug, + # see https://github.com/python/cpython/issues/65530 + res = res.encode('ISO-8859-1', errors='backslashreplace').decode('unicode_escape') + if is_fstring: return res, n_closing_chars return res diff --git a/hy/reader/mangling.py b/hy/reader/mangling.py index 5329be4f8..538c38c4f 100644 --- a/hy/reader/mangling.py +++ b/hy/reader/mangling.py @@ -1,4 +1,3 @@ -import keyword import re import unicodedata @@ -8,55 +7,43 @@ def mangle(s): - """Stringify the argument and convert it to a valid Python identifier - according to :ref:`Hy's mangling rules `. + """Stringify the argument (with :class:`str`, not :func:`repr` or + :hy:func:`hy.repr`) and convert it to a valid Python identifier according + to :ref:`Hy's mangling rules `. :: - If the argument is already both legal as a Python identifier and normalized - according to Unicode normalization form KC (NFKC), it will be returned - unchanged. Thus, ``mangle`` is idempotent. + (hy.mangle 'foo-bar) ; => "foo_bar" + (hy.mangle "🦑") ; => "hyx_XsquidX" - Examples: - :: + If the stringified argument is already both legal as a Python identifier + and normalized according to Unicode normalization form KC (NFKC), it will + be returned unchanged. Thus, ``hy.mangle`` is idempotent. :: - => (hy.mangle 'foo-bar) - "foo_bar" + (setv x '♦-->♠) + (= (hy.mangle (hy.mangle x)) (hy.mangle x)) ; => True - => (hy.mangle 'foo-bar?) - "is_foo_bar" - - => (hy.mangle '*) - "hyx_XasteriskX" - - => (hy.mangle '_foo/a?) - "_hyx_is_fooXsolidusXa" - - => (hy.mangle '-->) - "hyx_XhyphenHminusX_XgreaterHthan_signX" - - => (hy.mangle '<--) - "hyx_XlessHthan_signX__" + Generally, the stringifed input is expected to be parsable as a symbol. As + a convenience, it can also have the syntax of a :ref:`dotted identifier + `, and ``hy.mangle`` will mangle the dot-delimited + parts separately. :: + (hy.mangle "a.c!.d") ; => "a.hyx_cXexclamation_markX.d" """ assert s s = str(s) - if "." in s: + if "." in s and s.strip("."): return ".".join(mangle(x) if x else "" for x in s.split(".")) - # Step 1: Remove and save leading underscores + # Remove and save leading underscores s2 = s.lstrip(normalizes_to_underscore) leading_underscores = "_" * (len(s) - len(s2)) s = s2 - # Step 2: Convert hyphens without introducing a new leading underscore + # Convert hyphens without introducing a new leading underscore s = s[0] + s[1:].replace("-", "_") if s else s - # Step 3: Convert trailing `?` to leading `is_` - if s.endswith("?"): - s = "is_" + s[:-1] - - # Step 4: Convert invalid characters or reserved words + # Convert invalid characters or reserved words if not (leading_underscores + s).isidentifier(): # Replace illegal characters with their Unicode character # names, or hexadecimal if they don't have one. @@ -84,38 +71,19 @@ def mangle(s): def unmangle(s): """Stringify the argument and try to convert it to a pretty unmangled - form. See :ref:`Hy's mangling rules `. + form. See :ref:`Hy's mangling rules `. :: + + (hy.unmangle "hyx_XsquidX") ; => "🦑" Unmangling may not round-trip, because different Hy symbol names can mangle to the same Python identifier. In particular, Python itself already considers distinct strings that have the same normalized form (according to NFKC), such as ``hello`` and ``𝔥𝔢𝔩𝔩𝔬``, to be the same identifier. - Examples: - :: - - => (hy.unmangle 'foo_bar) - "foo-bar" - - => (hy.unmangle 'is_foo_bar) - "foo-bar?" - - => (hy.unmangle 'hyx_XasteriskX) - "*" - - => (hy.unmangle '_hyx_is_fooXsolidusXa) - "_foo/a?" - - => (hy.unmangle 'hyx_XhyphenHminusX_XgreaterHthan_signX) - "-->" - - => (hy.unmangle 'hyx_XlessHthan_signX__) - "<--" - - => (hy.unmangle '__dunder_name__) - "__dunder-name__" - - """ + It's an error to call ``hy.unmangle`` on something that looks like a + properly mangled name but isn't. For example, ``(hy.unmangle + "hyx_XpizzazzX")`` is erroneous, because there is no Unicode character + named "PIZZAZZ" (yet).""" s = str(s) @@ -135,8 +103,14 @@ def unmangle(s): ), s[len("hyx_") :], ) - if s.startswith("is_"): - s = s[len("is_") :] + "?" 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))) diff --git a/hy/reader/reader.py b/hy/reader/reader.py index b2282f597..2651945c3 100644 --- a/hy/reader/reader.py +++ b/hy/reader/reader.py @@ -4,9 +4,8 @@ import re from collections import deque from contextlib import contextmanager -from io import StringIO -from .exceptions import LexException, PrematureEndOfInput +from .exceptions import PrematureEndOfInput _whitespace = re.compile(r"[ \t\n\r\f\v]+") diff --git a/hy/repl.py b/hy/repl.py new file mode 100644 index 000000000..47aa14c44 --- /dev/null +++ b/hy/repl.py @@ -0,0 +1,454 @@ +import ast +import builtins +import code +import codeop +import hashlib +import importlib +import linecache +import os +import platform +import sys +import time +import traceback +import types +from contextlib import contextmanager + +import hy +from hy.compiler import HyASTCompiler, hy_compile +from hy.completer import Completer, completion +from hy.errors import ( + HyLanguageError, + HyMacroExpansionError, + HyRequireError, + filtered_hy_exceptions, +) +from hy.importer import HyLoader +from hy.macros import enable_readers, require, require_reader +from hy.reader import mangle, read_many +from hy.reader.exceptions import PrematureEndOfInput +from hy.reader.hy_reader import HyReader + + +class HyQuitter: + def __init__(self, name): + self.name = name + + def __repr__(self): + return "Use (%s) or Ctrl-D (i.e. EOF) to exit" % (self.name) + + __str__ = __repr__ + + def __call__(self, code=None): + try: + sys.stdin.close() + except: + pass + raise SystemExit(code) + + +class HyHelper: + def __repr__(self): + return ( + "Use (help) for interactive help, or (help object) for help " + "about object." + ) + + def __call__(self, *args, **kwds): + import pydoc + + return pydoc.help(*args, **kwds) + + +sys.last_type = None +sys.last_value = None +sys.last_traceback = None + + +@contextmanager +def extend_linecache(add_cmdline_cache): + _linecache_checkcache = linecache.checkcache + + def _cmdline_checkcache(*args): + _linecache_checkcache(*args) + linecache.cache.update(add_cmdline_cache) + + linecache.checkcache = _cmdline_checkcache + yield + linecache.checkcache = _linecache_checkcache + + +_codeop_maybe_compile = codeop._maybe_compile + + +def _hy_maybe_compile(compiler, source, filename, symbol): + """The `codeop` version of this will compile the same source multiple + times, and, since we have macros and things like `eval-and-compile`, we + can't allow that. + """ + if not isinstance(compiler, HyCompile): + return _codeop_maybe_compile(compiler, source, filename, symbol) + + for line in source.split("\n"): + line = line.strip() + if line and line[0] != ";": + # Leave it alone (could do more with Hy syntax) + break + else: + if symbol != "eval": + # Replace it with a 'pass' statement (i.e. tell the compiler to do + # nothing) + source = "pass" + + return compiler(source, filename, symbol) + + +codeop._maybe_compile = _hy_maybe_compile + + +class HyCompile(codeop.Compile): + """This compiler uses `linecache` like + `IPython.core.compilerop.CachingCompiler`. + """ + + def __init__( + self, module, locals, ast_callback=None, hy_compiler=None, cmdline_cache={} + ): + self.module = module + self.locals = locals + self.ast_callback = ast_callback + self.hy_compiler = hy_compiler + self.reader = HyReader() + self.skip_next_shebang = False + + super().__init__() + + if hasattr(self.module, "_hy_reader_macros"): + enable_readers( + self.module, self.reader, self.module._hy_reader_macros.keys() + ) + + self.cmdline_cache = cmdline_cache + + def _cache(self, source, name): + entry = ( + len(source), + time.time(), + [line + "\n" for line in source.splitlines()], + name, + ) + + linecache.cache[name] = entry + self.cmdline_cache[name] = entry + + def _update_exc_info(self): + self.locals["_hy_last_type"] = sys.last_type + self.locals["_hy_last_value"] = sys.last_value + # Skip our frame. + sys.last_traceback = getattr(sys.last_traceback, "tb_next", sys.last_traceback) + self.locals["_hy_last_traceback"] = sys.last_traceback + + def __call__(self, source, filename="", symbol="single"): + + if source == "pass": + # We need to return a no-op to signal that no more input is needed. + return (compile(source, filename, symbol),) * 2 + + hash_digest = hashlib.sha1(source.encode("utf-8").strip()).hexdigest() + name = "{}-{}".format(filename.strip("<>"), hash_digest) + + self._cache(source, name) + + try: + root_ast = ast.Interactive if symbol == "single" else ast.Module + + # Our compiler doesn't correspond to a real, fixed source file, so + # we need to [re]set these. + self.hy_compiler.filename = name + self.hy_compiler.source = source + hy_ast = read_many( + source, filename=name, reader=self.reader, + skip_shebang=self.skip_next_shebang, + ) + self.skip_next_shebang = False + exec_ast, eval_ast = hy_compile( + hy_ast, + self.module, + root=root_ast, + get_expr=True, + compiler=self.hy_compiler, + filename=name, + source=source, + import_stdlib=False, + ) + + if self.ast_callback: + self.ast_callback(exec_ast, eval_ast) + + exec_code = super().__call__(exec_ast, name, symbol) + eval_code = super().__call__(eval_ast, name, "eval") + + except Exception as e: + # Capture and save the error before we handle further + + sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() + self._update_exc_info() + + if isinstance(e, (PrematureEndOfInput, SyntaxError)): + raise + else: + # Hy will raise exceptions during compile-time that Python would + # raise during run-time (e.g. import errors for `require`). In + # order to work gracefully with the Python world, we convert such + # Hy errors to code that purposefully reraises those exceptions in + # the places where Python code expects them. + # Capture a traceback without the compiler/REPL frames. + exec_code = super(HyCompile, self).__call__( + "raise _hy_last_value.with_traceback(_hy_last_traceback)", + name, + symbol, + ) + eval_code = super(HyCompile, self).__call__("None", name, "eval") + + return exec_code, eval_code + + +class HyCommandCompiler(codeop.CommandCompiler): + def __init__(self, *args, allow_incomplete = True, **kwargs): + self.compiler = HyCompile(*args, **kwargs) + self.allow_incomplete = allow_incomplete + + def __call__(self, *args, **kwargs): + try: + return super().__call__(*args, **kwargs) + except PrematureEndOfInput: + # We have to do this here, because `codeop._maybe_compile` won't + # take `None` for a return value (at least not in Python 2.7) and + # this exception type is also a `SyntaxError`, so it will be caught + # by `code.InteractiveConsole` base methods before it reaches our + # `runsource`. + if not self.allow_incomplete: + raise + + +class REPL(code.InteractiveConsole): + """A subclass of :class:`code.InteractiveConsole` for Hy. + + A convenient way to use this class to interactively debug code is to insert the + following in the code you want to debug:: + + (.run (hy.REPL :locals (locals))) + + Note that as with :func:`code.interact`, changes to ``(locals)`` inside the REPL are + not propagated back to the original scope.""" + + def __init__(self, spy=False, spy_delimiter=('-' * 30), output_fn=None, locals=None, filename="", allow_incomplete=True): + + # Create a proper module for this REPL so that we can obtain it easily + # (e.g. using `importlib.import_module`). + # We let `InteractiveConsole` initialize `self.locals` when it's + # `None`. + super().__init__(locals=locals, filename=filename) + + module_name = self.locals.get("__name__", "__console__") + # Make sure our newly created module is properly introduced to + # `sys.modules`, and consistently use its namespace as `self.locals` + # from here on. + self.module = sys.modules.setdefault(module_name, types.ModuleType(module_name)) + self.module.__dict__.update(self.locals) + self.locals = self.module.__dict__ + + if os.environ.get("HYSTARTUP"): + try: + loader = HyLoader("__hystartup__", os.environ.get("HYSTARTUP")) + spec = importlib.util.spec_from_loader(loader.name, loader) + mod = importlib.util.module_from_spec(spec) + sys.modules.setdefault(mod.__name__, mod) + loader.exec_module(mod) + imports = mod.__dict__.get( + "__all__", + [name for name in mod.__dict__ if not name.startswith("_")], + ) + imports = {name: mod.__dict__[name] for name in imports} + spy = spy or imports.get("repl_spy") + output_fn = output_fn or imports.get("repl_output_fn") + + # Load imports and defs + self.locals.update(imports) + + # load module macros + require(mod, self.module, assignments="ALL") + require_reader(mod, self.module, assignments="ALL") + except Exception as e: + print(e) + + self.hy_compiler = HyASTCompiler(self.module, module_name) + + self.cmdline_cache = {} + self.compile = HyCommandCompiler( + self.module, + self.locals, + ast_callback=self.ast_callback, + hy_compiler=self.hy_compiler, + cmdline_cache=self.cmdline_cache, + allow_incomplete=allow_incomplete, + ) + + self.spy = spy + self.spy_delimiter = spy_delimiter + self.last_value = None + self.print_last_value = True + + if output_fn is None: + self.output_fn = hy.repr + elif callable(output_fn): + self.output_fn = output_fn + elif "." in output_fn: + parts = [mangle(x) for x in output_fn.split(".")] + module, f = ".".join(parts[:-1]), parts[-1] + self.output_fn = getattr(importlib.import_module(module), f) + else: + self.output_fn = getattr(builtins, mangle(output_fn)) + + # Pre-mangle symbols for repl recent results: *1, *2, *3 + self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)] + self.locals.update({sym: None for sym in self._repl_results_symbols}) + + # Allow access to the running REPL instance + self.locals["_hy_repl"] = self + + # Compile an empty statement to load the standard prelude + exec_ast = hy_compile( + read_many(""), self.module, compiler=self.hy_compiler, import_stdlib=True + ) + if self.ast_callback: + self.ast_callback(exec_ast, None) + + def ast_callback(self, exec_ast, eval_ast): + if self.spy: + try: + # Mush the two AST chunks into a single module for + # conversion into Python. + new_ast = ast.Module( + exec_ast.body + + ([] if eval_ast is None else [ast.Expr(eval_ast.body)]), + type_ignores=[], + ) + print(ast.unparse(new_ast)) + print(self.spy_delimiter) + except Exception: + msg = "Exception in AST callback:\n{}\n".format(traceback.format_exc()) + self.write(msg) + + def _error_wrap(self, exc_info_override=False, *args, **kwargs): + sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() + + if exc_info_override: + # Use a traceback that doesn't have the REPL frames. + sys.last_type = self.locals.get("_hy_last_type", sys.last_type) + sys.last_value = self.locals.get("_hy_last_value", sys.last_value) + sys.last_traceback = self.locals.get( + "_hy_last_traceback", sys.last_traceback + ) + + sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) + + self.locals[mangle("*e")] = sys.last_value + + def showsyntaxerror(self, filename=None): + if filename is None: + filename = self.filename + self.print_last_value = False + + self._error_wrap(exc_info_override=True, filename=filename) + + def showtraceback(self): + self._error_wrap() + + def runcode(self, code): + try: + eval(code[0], self.locals) + self.last_value = eval(code[1], self.locals) + # Don't print `None` values. + self.print_last_value = self.last_value is not None + except SystemExit: + raise + except Exception as e: + # Set this to avoid a print-out of the last value on errors. + self.print_last_value = False + self.showtraceback() + + def runsource(self, source, filename="", symbol="exec"): + try: + res = super().runsource(source, filename, symbol) + except (HyMacroExpansionError, HyRequireError): + # We need to handle these exceptions ourselves, because the base + # method only handles `OverflowError`, `SyntaxError` and + # `ValueError`. + self.showsyntaxerror(filename) + return False + except (HyLanguageError): + # Our compiler will also raise `TypeError`s + self.showtraceback() + return False + + # Shift exisitng REPL results + if not res: + next_result = self.last_value + for sym in self._repl_results_symbols: + self.locals[sym], next_result = next_result, self.locals[sym] + + # Print the value. + if self.print_last_value: + try: + output = self.output_fn(self.last_value) + except Exception: + self.showtraceback() + return False + + print(output) + + return res + + def run(self): + "Start running the REPL. Return 0 when done." + + sentinel = [] + saved_values = ( + getattr(sys, "ps1", sentinel), + getattr(sys, "ps2", sentinel), + builtins.quit, + builtins.exit, + builtins.help, + ) + try: + sys.ps1 = "=> " + sys.ps2 = "... " + builtins.quit = HyQuitter("quit") + builtins.exit = HyQuitter("exit") + builtins.help = HyHelper() + + namespace = self.locals + with filtered_hy_exceptions(), extend_linecache( + self.cmdline_cache + ), completion(Completer(namespace)): + self.interact(self.banner()) + + finally: + sys.ps1, sys.ps2, builtins.quit, builtins.exit, builtins.help = saved_values + for a in "ps1", "ps2": + if getattr(sys, a) is sentinel: + delattr(sys, a) + + return 0 + + def banner(self): + return "Hy {version} using {py}({build}) {pyversion} on {os}".format( + version=hy.__version__, + py=platform.python_implementation(), + build=platform.python_build()[0], + pyversion=platform.python_version(), + os=platform.system(), + ) + + +REPL.__module__ = "hy" # Print as `hy.REPL` instead of `hy.repl.REPL`. diff --git a/hy/reserved.hy b/hy/reserved.hy deleted file mode 100644 index f20cae24a..000000000 --- a/hy/reserved.hy +++ /dev/null @@ -1,33 +0,0 @@ -;;; Get a frozenset of Hy reserved words - -(import sys keyword) - -(setv _cache None) - -(defn macros [] - "Return a frozenset of Hy's core macro names." - (frozenset (map hy.unmangle (+ - (list (.keys hy.core.result_macros.__macros__)) - (list (.keys hy.core.macros.__macros__)))))) - -(defn names [] - "Return a frozenset of reserved symbol names. - - The result of the first call is cached. - - The output includes all of Hy's core functions and macros, plus all - Python reserved words. All names are in unmangled form (e.g., - ``not-in`` rather than ``not_in``). - - Examples: - :: - - => (import hy.extra.reserved) - => (in \"defclass\" (hy.extra.reserved.names)) - True - " - (global _cache) - (when (is _cache None) - (setv _cache (| (macros) (frozenset (map hy.unmangle - keyword.kwlist))))) - _cache) diff --git a/hy/scoping.py b/hy/scoping.py index 3fd8f2daf..fbfa4e6b4 100644 --- a/hy/scoping.py +++ b/hy/scoping.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod import hy._compat -from hy.errors import HyInternalError from hy.models import Expression, List, Symbol, Tuple from hy.reader import mangle @@ -32,6 +31,45 @@ def nearest_python_scope(scope): return cur +class OuterVar(ast.stmt): + "Custom AST node that can compile to either Nonlocal or Global." + def __init__(self, expr, scope, names): + from hy.compiler import asty + super().__init__() + self.__dict__.update(asty._get_pos(expr)) + self._scope = scope + self.names = names + + +class ResolveOuterVars(ast.NodeTransformer): + "Find all OuterVar nodes and replace with Nonlocal or Global as necessary." + def visit_OuterVar(self, node): + from hy.compiler import asty + scope = node._scope + defined = set() + undefined = list(node.names) # keep order, so can't use set + while undefined and scope.parent: + scope = scope.parent + has = set() + if isinstance(scope, ScopeFn): + has = scope.defined + elif isinstance(scope, ScopeLet): + has = set(scope.bindings.keys()) + elif isinstance(scope, ScopeGlobal): + res = [] + if not scope.defined.issuperset(undefined): + # emit nonlocal, let python raise the error + break + if undefined: + res.append(asty.Global(node, names=list(undefined))) + if defined: + res.append(asty.Nonlocal(node, names=list(defined))) + return res + defined.update(has.intersection(undefined)) + undefined = [name for name in undefined if name not in has] + return [asty.Nonlocal(node, names=node.names)] if node.names else [] + + class NodeRef: """ Wrapper for AST nodes that have symbol names, so that we can rename them if @@ -45,6 +83,7 @@ class NodeRef: ast.Name: "id", ast.Global: "names", ast.Nonlocal: "names", + OuterVar: "names", } if hy._compat.PY3_10: ACCESSOR.update( @@ -97,19 +136,23 @@ class ScopeBase(ABC): def __init__(self, compiler): self.parent = None self.compiler = compiler + self.children = [] def create(self, scope_type, *args): "Create new scope from this one." return scope_type(self.compiler, *args) def __enter__(self): - self.parent = self.compiler.scope + if self.compiler.scope is not self: + self.parent = self.compiler.scope + if self not in self.parent.children: + self.parent.children.append(self) self.compiler.scope = self return self def __exit__(self, *args): - self.compiler.scope = self.parent - self.parent = None + if self.parent: + self.compiler.scope = self.parent return False # Scope interface @@ -249,19 +292,25 @@ def add(self, target, new_name=None): class ScopeFn(ScopeBase): """Scope that corresponds to Python's own function or class scopes.""" - def __init__(self, compiler, args=None): + def __init__(self, compiler, args=None, is_async=False): super().__init__(compiler) self.defined = set() "set: of all vars defined in this scope" self.seen = [] "list: of all vars accessedto in this scope" - self.nonlocal_vars = set() + self.nonlocal_vars = {} "set: of all `nonlocal`'s defined in this scope" self.is_fn = args is not None """ bool: `True` if this scope is being used to track a python function `False` for classes """ + self.is_async = is_async + """bool: `True` if this scope is for an async function, + which may need special handling during compilation""" + self.has_yield = False + """bool: `True` if this scope is tracking a function that has `yield` + statements, as generator functions may need special handling""" if args: for arg in itertools.chain( @@ -271,8 +320,9 @@ def __init__(self, compiler, args=None): self.define(arg.arg) def __exit__(self, *args): + self.defined.difference_update(self.nonlocal_vars.keys()) for node in self.seen: - if node.name not in self.defined or node.name in self.nonlocal_vars: + if node.name not in self.defined: # pass unbound/nonlocal names up to parent scope self.parent.access(node) return super().__exit__(*args) @@ -292,11 +342,11 @@ def define(self, name): self.defined.add(name) def define_nonlocal(self, node, root): - ( - (self.nonlocal_vars if root == "nonlocal" else self.defined).update( - node.names - ) - ) + if root == "nonlocal": + self.nonlocal_vars.update({name: node for name in node.names}) + else: + self.defined.update(node.names) + for n in self.seen: if n.name in node.names: raise SyntaxError( @@ -324,7 +374,6 @@ def __init__(self, compiler): super().__init__(compiler) self.iterators = set() self.assignments = [] - self.nonlocals = set() self.exposing_assignments = False def __enter__(self): diff --git a/requirements-dev.txt b/requirements-dev.txt index e7802ba88..a28ba3224 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,7 @@ -pytest >= 6 - -# autoformatting -black==22.3.0 -isort==5.10.1 -pre_commit==2.17.0 +pytest >= 7 # documentation -Pygments >= 2 -Sphinx >= 5 -sphinx_rtd_theme >= 1 +Pygments == 2.15.1 +Sphinx == 5.0.2 +sphinx_rtd_theme == 1.2.2 git+https://github.com/hylang/sphinxcontrib-hydomain.git diff --git a/setup.cfg b/setup.cfg index 4f33fc84c..20f7df279 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,8 @@ [tool:pytest] # Be sure to include Hy test functions with mangled names. -python_functions=test_* is_test_* hyx_test_* hyx_is_test_* +python_functions=test_* hyx_test_* filterwarnings = once::DeprecationWarning once::PendingDeprecationWarning ignore::SyntaxWarning + ignore::pytest.PytestReturnNotNoneWarning diff --git a/setup.py b/setup.py index 92e0ea64c..4f7166277 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,14 @@ #!/usr/bin/env python +# Set both `setup_requires` and `install_requires` with our +# dependencies, since we need to compile Hy files during setup. And +# put this as the first statement in the file so it's easy to parse +# out without executing the file. +requires = [ + "funcparserlib ~= 1.0", + 'astor>=0.8 ; python_version < "3.9"', +] + import os import fastentrypoints # Monkey-patches setuptools. @@ -31,29 +40,17 @@ def run(self): invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH, ) - -# both setup_requires and install_requires -# since we need to compile .hy files during setup -requires = [ - "funcparserlib ~= 1.0", - "colorama", - 'astor>=0.8 ; python_version < "3.9"', -] - setup( name=PKG, version="0.24.2", setup_requires=["wheel"] + requires, install_requires=requires, - python_requires=">= 3.7, < 3.12", + python_requires=">= 3.8, < 3.13", entry_points={ "console_scripts": [ "hy = hy.cmdline:hy_main", - "hy3 = hy.cmdline:hy_main", "hyc = hy.cmdline:hyc_main", - "hyc3 = hy.cmdline:hyc_main", - "hy2py = hy.cmdline:hy2py_main", - "hy2py3 = hy.cmdline:hy2py_main", + "hy2py = hy.cmdline:hy2py_main" ] }, packages=find_packages(exclude=["tests*"]), @@ -77,10 +74,13 @@ def run(self): "Programming Language :: Lisp", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: PyPy", + "Environment :: WebAssembly :: Emscripten", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Compilers", "Topic :: Software Development :: Libraries", diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index c38e55b73..001c2e55f 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -1,10 +1,11 @@ -# fmt: off - import ast +from textwrap import dedent import pytest -from hy.compiler import hy_compile, hy_eval +import hy +from hy._compat import PY3_11 +from hy.compiler import hy_compile from hy.errors import HyError, HyLanguageError from hy.reader import read_many from hy.reader.exceptions import LexException, PrematureEndOfInput @@ -17,12 +18,14 @@ def _ast_spotcheck(arg, root, secondary): assert getattr(root, arg) == getattr(secondary, arg) -def can_compile(expr, import_stdlib=False): - return hy_compile(read_many(expr), __name__, import_stdlib=import_stdlib) +def can_compile(expr, import_stdlib=False, iff=True): + return (hy_compile(read_many(expr), __name__, import_stdlib=import_stdlib) + if iff + else cant_compile(expr)) def can_eval(expr): - return hy_eval(read_many(expr)) + return hy.eval(read_many(expr)) def cant_compile(expr): @@ -36,7 +39,7 @@ def cant_compile(expr): def s(x): - return can_compile('"module docstring" ' + x).body[-1].value.s + return can_compile('"module docstring" ' + x).body[-1].value.value def test_ast_bad_type(): @@ -67,45 +70,25 @@ def test_dot_unpacking(): def test_ast_bad_if(): - "Make sure AST can't compile invalid if" cant_compile("(if)") cant_compile("(if foobar)") cant_compile("(if 1 2 3 4 5)") def test_ast_valid_if(): - "Make sure AST can compile valid if" can_compile("(if foo bar baz)") -def test_ast_valid_unary_op(): - "Make sure AST can compile valid unary operator" - can_compile("(not 2)") - can_compile("(~ 1)") - - -def test_ast_invalid_unary_op(): - "Make sure AST can't compile invalid unary operator" - cant_compile("(not 2 3 4)") - cant_compile("(not)") - cant_compile("(not 2 3 4)") - cant_compile("(~ 2 2 3 4)") - cant_compile("(~)") - - def test_ast_bad_while(): - "Make sure AST can't compile invalid while" cant_compile("(while)") def test_ast_good_do(): - "Make sure AST can compile valid do" can_compile("(do)") can_compile("(do 1)") def test_ast_good_raise(): - "Make sure AST can compile valid raise" can_compile("(raise)") can_compile("(raise Exception)") can_compile("(raise e)") @@ -116,36 +99,30 @@ def test_ast_raise_from(): def test_ast_bad_raise(): - "Make sure AST can't compile invalid raise" cant_compile("(raise Exception Exception)") def test_ast_good_try(): - "Make sure AST can compile valid try" can_compile("(try 1 (except []) (else 1))") can_compile("(try 1 (finally 1))") can_compile("(try 1 (except []) (finally 1))") can_compile("(try 1 (except [x]) (except [y]) (finally 1))") can_compile("(try 1 (except []) (else 1) (finally 1))") can_compile("(try 1 (except [x]) (except [y]) (else 1) (finally 1))") + can_compile(iff = PY3_11, expr = "(try 1 (except* [x]))") + can_compile(iff = PY3_11, expr = "(try 1 (except* [x]) (else 1) (finally 1))") def test_ast_bad_try(): - "Make sure AST can't compile invalid try" - cant_compile("(try)") - cant_compile("(try 1)") - cant_compile("(try 1 bla)") - cant_compile("(try 1 bla bla)") - cant_compile("(try (do bla bla))") cant_compile("(try (do) (else 1) (else 2))") - cant_compile("(try 1 (else 1))") cant_compile("(try 1 (else 1) (except []))") cant_compile("(try 1 (finally 1) (except []))") cant_compile("(try 1 (except []) (finally 1) (else 1))") + cant_compile("(try 1 (except* [x]) (except [x]))") + cant_compile("(try 1 (except [x]) (except* [x]))") def test_ast_good_except(): - "Make sure AST can compile valid except" can_compile("(try 1 (except []))") can_compile("(try 1 (except [Foobar]))") can_compile("(try 1 (except [[]]))") @@ -155,7 +132,6 @@ def test_ast_good_except(): def test_ast_bad_except(): - "Make sure AST can't compile invalid except" cant_compile("(except 1)") cant_compile("(try 1 (except))") cant_compile("(try 1 (except 1))") @@ -165,8 +141,6 @@ def test_ast_bad_except(): def test_ast_good_assert(): - """Make sure AST can compile valid asserts. Asserts may or may not - include a label.""" can_compile("(assert 1)") can_compile('(assert 1 "Assert label")') can_compile('(assert 1 (+ "spam " "eggs"))') @@ -176,38 +150,32 @@ def test_ast_good_assert(): def test_ast_bad_assert(): - "Make sure AST can't compile invalid assert" cant_compile("(assert)") cant_compile("(assert 1 2 3)") cant_compile("(assert 1 [1 2] 3)") def test_ast_good_global(): - "Make sure AST can compile valid global" + can_compile("(global)") can_compile("(global a)") can_compile("(global foo bar)") def test_ast_bad_global(): - "Make sure AST can't compile invalid global" - cant_compile("(global)") cant_compile("(global (foo))") def test_ast_good_nonlocal(): - "Make sure AST can compile valid nonlocal" + can_compile("(nonlocal)") can_compile("(do (setv a 0) (nonlocal a))") can_compile("(do (setv foo 0 bar 0) (nonlocal foo bar))") def test_ast_bad_nonlocal(): - "Make sure AST can't compile invalid nonlocal" - cant_compile("(nonlocal)") cant_compile("(nonlocal (foo))") def test_ast_good_defclass(): - "Make sure AST can compile valid defclass" can_compile("(defclass a)") can_compile("(defclass a [])") can_compile("(defclass a [] None 42)") @@ -216,13 +184,11 @@ def test_ast_good_defclass(): def test_ast_good_defclass_with_metaclass(): - "Make sure AST can compile valid defclass with keywords" can_compile("(defclass a [:metaclass b])") can_compile("(defclass a [:b c])") def test_ast_bad_defclass(): - "Make sure AST can't compile invalid defclass" cant_compile("(defclass)") cant_compile("(defclass a None)") cant_compile("(defclass a None None)") @@ -233,13 +199,11 @@ def test_ast_bad_defclass(): def test_ast_good_lambda(): - "Make sure AST can compile valid lambda" can_compile("(fn [])") can_compile("(fn [] 1)") def test_ast_bad_lambda(): - "Make sure AST can't compile invalid lambda" cant_compile("(fn)") cant_compile("(fn ())") cant_compile("(fn () 1)") @@ -248,12 +212,10 @@ def test_ast_bad_lambda(): def test_ast_good_yield(): - "Make sure AST can compile valid yield" can_compile("(yield 1)") def test_ast_bad_yield(): - "Make sure AST can't compile invalid yield" cant_compile("(yield 1 2)") @@ -266,12 +228,10 @@ def test_ast_import_mangle_dotted(): def test_ast_good_import_from(): - "Make sure AST can compile valid selective import" can_compile("(import x [y])") def test_ast_require(): - "Make sure AST respects (require) syntax" can_compile("(require tests.resources.tlib)") can_compile("(require tests.resources.tlib [qplah parald])") can_compile("(require tests.resources.tlib *)") @@ -303,18 +263,15 @@ def test_ast_multi_require(): def test_ast_good_get(): - "Make sure AST can compile valid get" can_compile("(get x y)") def test_ast_bad_get(): - "Make sure AST can't compile invalid get" cant_compile("(get)") cant_compile("(get 1)") def test_ast_good_cut(): - "Make sure AST can compile valid cut" can_compile("(cut x)") can_compile("(cut x y)") can_compile("(cut x y z)") @@ -322,26 +279,22 @@ def test_ast_good_cut(): def test_ast_bad_cut(): - "Make sure AST can't compile invalid cut" cant_compile("(cut)") cant_compile("(cut 1 2 3 4 5)") def test_ast_bad_with(): - "Make sure AST can't compile invalid with" cant_compile("(with)") cant_compile("(with [])") cant_compile("(with [] (pass))") def test_ast_valid_while(): - "Make sure AST can't compile invalid while" can_compile("(while foo bar)") can_compile("(while foo bar (else baz))") def test_ast_valid_for(): - "Make sure AST can compile valid for" can_compile("(for [a 2] (print a))") @@ -372,7 +325,6 @@ def test_ast_expression_basics(): def test_ast_anon_fns_basics(): - """Ensure anon fns work.""" code = can_compile("(fn [x] (* x x))").body[0].value assert type(code) == ast.Lambda code = can_compile('(fn [x] (print "multiform") (* x x))').body[0] @@ -382,7 +334,6 @@ def test_ast_anon_fns_basics(): def test_ast_lambda_lists(): - """Ensure the compiler chokes on invalid lambda-lists""" cant_compile("(fn [[a b c]] a)") cant_compile("(fn [[1 2]] (list 1 2))") @@ -394,20 +345,17 @@ def test_ast_print(): def test_ast_tuple(): - """Ensure tuples work.""" code = can_compile("#(1 2 3)").body[0].value assert type(code) == ast.Tuple def test_lambda_list_keywords_rest(): - """Ensure we can compile functions with lambda list keywords.""" can_compile("(fn [x #* xs] (print xs))") cant_compile("(fn [x #* xs #* ys] (print xs))") can_compile("(fn [[a None] #* xs] (print xs))") def test_lambda_list_keywords_kwargs(): - """Ensure we can compile functions with #** kwargs.""" can_compile("(fn [x #** kw] (list x kw))") cant_compile("(fn [x #** xs #** ys] (list x xs ys))") can_compile("(fn [[x None] #** kw] (list x kw))") @@ -419,25 +367,22 @@ def test_lambda_list_keywords_kwonly(): for i, kwonlyarg_name in enumerate(("a", "b")): assert kwonlyarg_name == code.body[0].args.kwonlyargs[i].arg assert code.body[0].args.kw_defaults[0] is None - assert code.body[0].args.kw_defaults[1].n == 2 + assert code.body[0].args.kw_defaults[1].value == 2 def test_lambda_list_keywords_mixed(): - """Ensure we can mix them up.""" can_compile("(fn [x #* xs #** kw] (list x xs kw))") cant_compile('(fn [x #* xs &fasfkey {bar "baz"}])') can_compile("(fn [x #* xs kwoxs #** kwxs]" " (list x xs kwxs kwoxs))") def test_missing_keyword_argument_value(): - """Ensure the compiler chokes on missing keyword argument values.""" with pytest.raises(HyLanguageError) as excinfo: can_compile("((fn [x] x) :x)") assert excinfo.value.msg == "Keyword argument :x needs a value." def test_ast_unicode_strings(): - """Ensure we handle unicode strings correctly""" def _compile_string(s): hy_s = hy.models.String(s) @@ -447,8 +392,8 @@ def _compile_string(s): ) # We put hy_s in a list so it isn't interpreted as a docstring. - # code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Str(s=xxx)]))]) - return code.body[0].value.elts[0].s + # code == ast.Module(body=[ast.Expr(value=ast.List(elts=[ast.Constant(value=xxx)]))]) + return code.body[0].value.elts[0].value assert _compile_string("test") == "test" assert _compile_string("\u03b1\u03b2") == "\u03b1\u03b2" @@ -504,6 +449,15 @@ def test_literal_newlines(): assert s("#[[\rhello\rworld]]") == "hello\nworld" +def test_linear_boolop(): + """Operations like `(and 1 2 3)` should use only one `BoolOp`, + instead of an equivalent nested sequence of `BoolOp`s.""" + for op in ("and", "or"): + node = can_compile(f'({op} 1 2.0 True "hi" 5)').body[0].value + assert len(node.values) == 5 + assert all(isinstance(v, ast.Constant) for v in node.values) + + def test_compile_error(): """Ensure we get compile error in tricky cases""" with pytest.raises(HyLanguageError) as excinfo: @@ -524,7 +478,6 @@ def test_for_compile_error(): def test_attribute_access(): - """Ensure attribute access compiles correctly""" can_compile("(. foo bar baz)") can_compile("(. foo [bar] baz)") can_compile("(. foo bar [baz] [0] quux [frob])") @@ -532,25 +485,21 @@ def test_attribute_access(): cant_compile("(. foo bar :baz [0] quux [frob])") cant_compile("(. foo bar baz (0) quux [frob])") cant_compile("(. foo bar baz [0] quux {frob})") - cant_compile("(.. foo bar baz)") -def test_attribute_empty(): - """Ensure using dot notation with a non-expression is an error""" - cant_compile(".") +def test_misplaced_dots(): cant_compile("foo.") - cant_compile(".foo") - cant_compile('"bar".foo') - cant_compile("[2].foo") + cant_compile("foo..") + cant_compile("foo.bar.") + cant_compile("foo.bar..") + cant_compile("foo..bar") def test_bad_setv(): - """Ensure setv handles error cases""" cant_compile("(setv (a b) [1 2])") def test_defn(): - """Ensure that defn works correctly in various corner cases""" cant_compile('(defn "hy" [] 1)') cant_compile("(defn :hy [] 1)") can_compile("(defn &hy [] 1)") @@ -572,14 +521,22 @@ def test_setv_builtins(): ) -def test_top_level_unquote(): +def placeholder_macro(x, ename=None): with pytest.raises(HyLanguageError) as e: - can_compile("(unquote)") - assert "`unquote` is not allowed here" in e.value.msg + can_compile(f"({x})") + assert f"`{ename or x}` is not allowed here" in e.value.msg + + +def test_top_level_unquote(): + placeholder_macro("unquote") + placeholder_macro("unquote-splice") + placeholder_macro("unquote_splice", "unquote-splice") - with pytest.raises(HyLanguageError) as e: - can_compile("(unquote-splice)") - assert "`unquote-splice` is not allowed here" in e.value.msg + +def test_bad_exception(): + placeholder_macro("except") + placeholder_macro("except*") + placeholder_macro(hy.mangle("except*"), "except*") def test_lots_of_comment_lines(): @@ -588,23 +545,19 @@ def test_lots_of_comment_lines(): def test_compiler_macro_tag_try(): - """Check that try forms within defmacro are compiled correctly""" # https://github.com/hylang/hy/issues/1350 can_compile("(defmacro foo [] (try None (except [] None)) `())") def test_ast_good_yield_from(): - "Make sure AST can compile valid yield-from" can_compile("(yield-from [1 2])") def test_ast_bad_yield_from(): - "Make sure AST can't compile invalid yield-from" cant_compile("(yield-from)") def test_eval_generator_with_return(): - """Ensure generators with a return statement works.""" can_eval("(fn [] (yield 1) (yield 2) (return))") @@ -620,11 +573,48 @@ def test_futures_imports(): assert hy_ast.body[0].module == "__future__" -def test_inline_python(): - can_compile('(py "1 + 1")') +def test_py(): + def py(x): assert ( + ast.dump(can_compile(f'(py "{x}")')) == + ast.dump(ast.parse('(' + x + '\n)'))) + + py("1 + 1") + # https://github.com/hylang/hy/issues/2406 + py(" 1 + 1 ") + py(""" 1 + + 1 """) + py(""" 1 + 2 + + 3 + + 4 + + 5 + # hi! + 6 # bye """) + cant_compile('(py "1 +")') - can_compile('(pys "if 1:\n 2")') + cant_compile('(py "if 1:\n 2")') + + +def test_pys(): + def pys(x): assert ( + ast.dump(can_compile(f'(pys "{x}")')) == + ast.dump(ast.parse(dedent(x)))) + + pys("") + pys("1 + 1") + pys("if 1:\n 2") + pys("if 1: 2") + pys(" if 1: 2 ") + pys(''' + if 1: + 2 + elif 3: + 4''') + cant_compile('(pys "if 1\n 2")') + cant_compile('''(pys " + if 1: + 2 + elif 3: + 4")''') def test_models_accessible(): @@ -643,3 +633,21 @@ def test_module_prelude(): x = x[0].names[0] assert x.name == "hy" assert x.asname is None + + +def test_pragma(): + can_compile("(pragma)") + can_compile("(pragma :warn-on-core-shadow True)") + cant_compile("(pragma :native-code True)") + + +def test_error_with_expectation(): + def check(code, expected): + assert cant_compile(code).msg.endswith("expected: " + expected) + + check("(defmacro)", "Symbol") + check("(quote)", "form") + check("(py)", "String") + check("(py a)", "String") + check('(py "foo" a)', "end of macro call") + check('(for a)', "square-bracketed loop clauses") diff --git a/tests/compilers/test_compiler.py b/tests/compilers/test_compiler.py index 64fd2a01e..38b904417 100644 --- a/tests/compilers/test_compiler.py +++ b/tests/compilers/test_compiler.py @@ -3,6 +3,8 @@ from hy import compiler from hy.models import Expression, Integer, List, Symbol +from hy.reader import read_many +from hy.compiler import hy_compile def make_expression(*args): @@ -14,6 +16,10 @@ def make_expression(*args): return h.replace(h) +def hy2py(s): + return ast.unparse(hy_compile(read_many(s), "test", import_stdlib=False)) + + def test_compiler_bare_names(): """ Check that the compiler doesn't drop bare names from code branches @@ -58,3 +64,27 @@ def test_compiler_yield_return(): assert isinstance(body[0].value, ast.Yield) assert isinstance(body[1], ast.Return) assert isinstance(body[1].value, ast.BinOp) + + +# https://github.com/hylang/hy/issues/854 +def test_compact_logic(): + """ + Check that we don't generate multiple unnecessary if-statements + when compiling the short-circuiting operators. + """ + py = hy2py("(and 1 2 3 (do (setv x 4) x) 5 6)") + assert py.count("if") == 1 + py = hy2py("(or 1 2 3 (do (setv x 4) x) 5 6 (do (setv y 7)))") + assert py.count("if") == 2 + +def test_correct_logic(): + """ + Check that we're not overzealous when compacting boolean operators. + """ + py = hy2py("(or (and 1 2) (and 3 4))") + assert py.count("and") == 2 + assert py.count("or") == 1 + py = hy2py("(and (do (setv x 4) (or x 3)) 5 6)") + assert py.count("x = 4") == 1 + assert py.count("x or 3") == 1 + assert py.count("and") == 2 diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py index 3dcd1e08e..4a7d66c0c 100644 --- a/tests/importer/test_importer.py +++ b/tests/importer/test_importer.py @@ -8,7 +8,7 @@ import pytest import hy -from hy.compiler import hy_compile, hy_eval +from hy.compiler import hy_compile from hy.errors import HyLanguageError, hy_exc_handler from hy.importer import HyLoader from hy.reader import read_many @@ -103,7 +103,7 @@ def import_from_path(path): def test_eval(): def eval_str(s): - return hy_eval(hy.read(s), filename="", source=s) + return hy.eval(hy.read(s)) assert eval_str("[1 2 3]") == [1, 2, 3] assert eval_str('{"dog" "bark" "cat" "meow"}') == {"dog": "bark", "cat": "meow"} @@ -273,3 +273,19 @@ def test_filtered_importlib_frames(capsys): captured_w_filtering = capsys.readouterr()[-1].strip() assert "importlib._" not in captured_w_filtering + + +def test_zipimport(tmp_path): + from zipfile import ZipFile + + zpath = tmp_path / "archive.zip" + with ZipFile(zpath, "w") as o: + o.writestr("example.hy", '(setv x "Hy from ZIP")') + + try: + sys.path.insert(0, str(zpath)) + import example + finally: + sys.path = [p for p in sys.path if p != str(zpath)] + assert example.x == "Hy from ZIP" + assert example.__file__ == str(zpath / "example.hy") diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py index 4b4f9c848..d870a4d24 100644 --- a/tests/macros/test_macro_processor.py +++ b/tests/macros/test_macro_processor.py @@ -2,13 +2,13 @@ from hy.compiler import HyASTCompiler from hy.errors import HyMacroExpansionError -from hy.macros import macro, macroexpand, macroexpand_1 +from hy.macros import macro, macroexpand from hy.models import Expression, Float, List, String, Symbol from hy.reader import read @macro("test") -def tmac(ETname, *tree): +def tmac(*tree): """Turn an expression into a list""" return List(tree) @@ -40,7 +40,7 @@ def test_preprocessor_exceptions(): """Test that macro expansion raises appropriate exceptions""" with pytest.raises(HyMacroExpansionError) as excinfo: macroexpand(read("(when)"), __name__, HyASTCompiler(__name__)) - assert "_hy_anon_" not in excinfo.value.msg + assert "TypeError: when()" in excinfo.value.msg def test_macroexpand_nan(): @@ -55,9 +55,9 @@ def test_macroexpand_nan(): def test_macroexpand_source_data(): # https://github.com/hylang/hy/issues/1944 - ast = Expression([Symbol("#@"), String("a")]) + ast = Expression([Symbol("when"), String("a")]) ast.start_line = 3 ast.start_column = 5 - bad = macroexpand_1(ast, "hy.core.macros") + bad = macroexpand(ast, "hy.core.macros", once = True) assert bad.start_line == 3 assert bad.start_column == 5 diff --git a/tests/native_tests/language_beside.hy b/tests/native_tests/beside.hy similarity index 69% rename from tests/native_tests/language_beside.hy rename to tests/native_tests/beside.hy index 6594c5a85..cbe3a3206 100644 --- a/tests/native_tests/language_beside.hy +++ b/tests/native_tests/beside.hy @@ -1,5 +1,5 @@ ;; This file has no tests of its own, and only exists to be required -;; by `language.hy`, and sit in the same directory to test +;; by `import.hy`, and sit in the same directory to test ;; single-period relative `require`. (defmacro xyzzy [] diff --git a/tests/native_tests/break_continue.hy b/tests/native_tests/break_continue.hy new file mode 100644 index 000000000..5a422f613 --- /dev/null +++ b/tests/native_tests/break_continue.hy @@ -0,0 +1,12 @@ +(defn test-break-breaking [] + (defn holy-grail [] (for [x (range 10)] (when (= x 5) (break))) x) + (assert (= (holy-grail) 5))) + + +(defn test-continue-continuation [] + (setv y []) + (for [x (range 10)] + (when (!= x 5) + (continue)) + (.append y x)) + (assert (= y [5]))) diff --git a/tests/native_tests/comprehensions.hy b/tests/native_tests/comprehensions.hy index e3bfc4bef..bc354a4cb 100644 --- a/tests/native_tests/comprehensions.hy +++ b/tests/native_tests/comprehensions.hy @@ -1,6 +1,8 @@ (import types - pytest) + asyncio + pytest + tests.resources [async-test]) (defn test-comprehension-types [] @@ -261,8 +263,8 @@ (assert (= out "x1-x2-y1y2-z1-z2-"))) -(defmacro eval-isolated [#*body] - `(hy.eval '(do ~@body) :module "" :locals {})) +(defmacro eval-isolated [#* body] + `(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "") :locals {})) (defn test-lfor-nonlocal [] @@ -359,3 +361,53 @@ (assert (= x 2))) (bar) (assert (= x 19)))) + + +(defn test-for-do [] + (do (do (do (do (do (do (do (do (do (setv #(x y) #(0 0))))))))))) + (for [- [1 2]] + (do + (setv x (+ x 1)) + (setv y (+ y 1)))) + (assert (= y x 2))) + + +(defn test-for-else [] + (setv x 0) + (for [a [1 2]] + (setv x (+ x a)) + (else (setv x (+ x 50)))) + (assert (= x 53)) + + (setv x 0) + (for [a [1 2]] + (setv x (+ x a)) + (else)) + (assert (= x 3))) + + +(defn [async-test] test-for-async [] + (defn/a numbers [] + (for [i [1 2]] + (yield i))) + + (asyncio.run + ((fn/a [] + (setv x 0) + (for [:async a (numbers)] + (setv x (+ x a))) + (assert (= x 3)))))) + + +(defn [async-test] test-for-async-else [] + (defn/a numbers [] + (for [i [1 2]] + (yield i))) + + (asyncio.run + ((fn/a [] + (setv x 0) + (for [:async a (numbers)] + (setv x (+ x a)) + (else (setv x (+ x 50)))) + (assert (= x 53)))))) diff --git a/tests/native_tests/conditional.hy b/tests/native_tests/conditional.hy new file mode 100644 index 000000000..97d2cbaca --- /dev/null +++ b/tests/native_tests/conditional.hy @@ -0,0 +1,262 @@ +;; Tests of `if`, `cond`, `when`, and `while` + +(import + pytest) + + +(defn test-branching [] + (if True + (assert (= 1 1)) + (assert (= 2 1)))) + + +(defn test-branching-with-do [] + (if False + (assert (= 2 1)) + (do + (assert (= 1 1)) + (assert (= 1 1)) + (assert (= 1 1))))) + + +(defn test-branching-expr-count-with-do [] + "Ensure we execute the right number of expressions in a branch." + (setv counter 0) + (if False + (assert (= 2 1)) + (do + (setv counter (+ counter 1)) + (setv counter (+ counter 1)) + (setv counter (+ counter 1)))) + (assert (= counter 3))) + + +(defn test-cond [] + (cond + (= 1 2) (assert (is True False)) + (is None None) (do (setv x True) (assert x))) + (assert (is (cond) None)) + + (assert (= (cond + False 1 + [] 2 + True 8) 8)) + + (setv x 0) + (assert (is (cond False 1 [] 2 x 3) None)) + + (with [e (pytest.raises hy.errors.HyMacroExpansionError)] + (hy.eval '(cond 1))) + (assert (in "needs an even number of arguments" e.value.msg)) + + ; Make sure each test is only evaluated once, and `cond` + ; short-circuits. + (setv x 1) + (assert (= "first" (cond + (do (*= x 2) True) (do (*= x 3) "first") + (do (*= x 5) True) (do (*= x 7) "second")))) + (assert (= x 6))) + + +(defn test-if [] + (assert (= 1 (if 0 -1 1)))) + + +(defn test-returnable-ifs [] + (assert (= True (if True True True)))) + + +(defn test-if-return-branching [] + ; thanks, kirbyfan64 + (defn f [] + (if True (setv x 1) 2) + 1) + + (assert (= 1 (f)))) + + +(defn test-nested-if [] + (for [x (range 10)] + (if (in "foo" "foobar") + (do + (if True True True)) + (do + (if False False False))))) + + +(defn test-if-in-if [] + (assert (= 42 + (if (if 1 True False) + 42 + 43))) + (assert (= 43 + (if (if 0 True False) + 42 + 43)))) + + +(defn test-when [] + (assert (= (when True 1) 1)) + (assert (= (when True 1 2) 2)) + (assert (= (when True 1 3) 3)) + (assert (= (when False 2) None)) + (assert (= (when (= 1 2) 42) None)) + (assert (= (when (= 2 2) 42) 42)) + + (assert (is (when (do (setv x 3) True)) None)) + (assert (= x 3))) + + +(defn test-while-loop [] + (setv count 5) + (setv fact 1) + (while (> count 0) + (setv fact (* fact count)) + (setv count (- count 1))) + (assert (= count 0)) + (assert (= fact 120)) + + (setv l []) + (defn f [] + (.append l 1) + (len l)) + (while (!= (f) 4)) + (assert (= l [1 1 1 1])) + + (setv l []) + (defn f [] + (.append l 1) + (len l)) + (while (!= (f) 4) (do)) + (assert (= l [1 1 1 1])) + + ; only compile the condition once + ; https://github.com/hylang/hy/issues/1790 + (global while-cond-var) + (setv while-cond-var 10) + (hy.eval + '(do + (defmacro while-cond [] + (global while-cond-var) + (assert (= while-cond-var 10)) + (+= while-cond-var 1) + `(do + (setv x 3) + False)) + (while (while-cond)) + (assert (= x 3))))) + + +(defn test-while-loop-else [] + (setv count 5) + (setv fact 1) + (setv myvariable 18) + (while (> count 0) + (setv fact (* fact count)) + (setv count (- count 1)) + (else (setv myvariable 26))) + (assert (= count 0)) + (assert (= fact 120)) + (assert (= myvariable 26)) + + ; multiple statements in a while loop should work + (setv count 5) + (setv fact 1) + (setv myvariable 18) + (setv myothervariable 15) + (while (> count 0) + (setv fact (* fact count)) + (setv count (- count 1)) + (else (setv myvariable 26) + (setv myothervariable 24))) + (assert (= count 0)) + (assert (= fact 120)) + (assert (= myvariable 26)) + (assert (= myothervariable 24)) + + ; else clause shouldn't get run after a break + (while True + (break) + (else (setv myvariable 53))) + (assert (= myvariable 26)) + + ; don't be fooled by constructs that look like else clauses + (setv x 2) + (setv a []) + (setv else True) + (while x + (.append a x) + (-= x 1) + [else (.append a "e")]) + (assert (= a [2 "e" 1 "e"])) + + (setv x 2) + (setv a []) + (with [(pytest.raises TypeError)] + (while x + (.append a x) + (-= x 1) + ("else" (.append a "e")))) + (assert (= a [2 "e"]))) + + +(defn test-while-multistatement-condition [] + + ; The condition should be executed every iteration, before the body. + ; `else` should be executed last. + (setv s "") + (setv x 2) + (while (do (+= s "a") x) + (+= s "b") + (-= x 1) + (else + (+= s "z"))) + (assert (= s "ababaz")) + + ; `else` should still be skipped after `break`. + (setv s "") + (setv x 2) + (while (do (+= s "a") x) + (+= s "b") + (-= x 1) + (when (= x 0) + (break)) + (else + (+= s "z"))) + (assert (= s "abab")) + + ; `continue` should jump to the condition. + (setv s "") + (setv x 2) + (setv continued? False) + (while (do (+= s "a") x) + (+= s "b") + (when (and (= x 1) (not continued?)) + (+= s "c") + (setv continued? True) + (continue)) + (-= x 1) + (else + (+= s "z"))) + (assert (= s "ababcabaz")) + + ; `break` in a condition applies to the `while`, not an outer loop. + (setv s "") + (for [x "123"] + (+= s x) + (setv y 0) + (while (do (when (and (= x "2") (= y 1)) (break)) (< y 3)) + (+= s "y") + (+= y 1))) + (assert (= s "1yyy2y3yyy")) + + ; The condition is still tested appropriately if its last variable + ; is set to a false value in the loop body. + (setv out []) + (setv x 0) + (setv a [1 1]) + (while (do (.append out 2) (setv x (and a (.pop a))) x) + (setv x 0) + (.append out x)) + (assert (= out [2 0 2 0 2])) + (assert (is x a))) diff --git a/tests/native_tests/core.hy b/tests/native_tests/core.hy deleted file mode 100644 index d70e7573a..000000000 --- a/tests/native_tests/core.hy +++ /dev/null @@ -1,161 +0,0 @@ -(import - itertools [repeat cycle islice] - pytest) - -;;;; some simple helpers - -(defn assert-true [x] - (assert (= True x))) - -(defn assert-false [x] - (assert (= False x))) - -(defn assert-equal [x y] - (assert (= x y))) - -(defn assert-none [x] - (assert (is x None))) - -(defn assert-requires-num [f] - (for [x ["foo" [] None]] - (try (f x) - (except [TypeError] True) - (else (assert False))))) - -(defn test-setv [] - (setv x 1) - (setv y 1) - (assert-equal x y) - (setv y 12) - (setv x y) - (assert-equal x 12) - (assert-equal y 12) - (setv y (fn [x] 9)) - (setv x y) - (assert-equal (x y) 9) - (assert-equal (y x) 9) - (try (do (setv a.b 1) (assert False)) - (except [e [NameError]] (assert (in "name 'a' is not defined" (str e))))) - (try (do (setv b.a (fn [x] x)) (assert False)) - (except [e [NameError]] (assert (in "name 'b' is not defined" (str e))))) - (import itertools) - (setv foopermutations (fn [x] (itertools.permutations x))) - (setv p (set [#(1 3 2) #(3 2 1) #(2 1 3) #(3 1 2) #(1 2 3) #(2 3 1)])) - (assert-equal (set (itertools.permutations [1 2 3])) p) - (assert-equal (set (foopermutations [3 1 2])) p) - (setv permutations- itertools.permutations) - (setv itertools.permutations (fn [x] 9)) - (assert-equal (itertools.permutations p) 9) - (assert-equal (foopermutations foopermutations) 9) - (setv itertools.permutations permutations-) - (assert-equal (set (itertools.permutations [2 1 3])) p) - (assert-equal (set (foopermutations [2 3 1])) p)) - -(setv globalvar 1) -(defn test-exec [] - (setv localvar 1) - (setv code " -result['localvar in locals'] = 'localvar' in locals() -result['localvar in globals'] = 'localvar' in globals() -result['globalvar in locals'] = 'globalvar' in locals() -result['globalvar in globals'] = 'globalvar' in globals() -result['x in locals'] = 'x' in locals() -result['x in globals'] = 'x' in globals() -result['y in locals'] = 'y' in locals() -result['y in globals'] = 'y' in globals()") - - (setv result {}) - (exec code) - (assert-true (get result "localvar in locals")) - (assert-false (get result "localvar in globals")) - (assert-false (get result "globalvar in locals")) - (assert-true (get result "globalvar in globals")) - (assert-false (or - (get result "x in locals") (get result "x in globals") - (get result "y in locals") (get result "y in globals"))) - - (setv result {}) - (exec code {"x" 1 "result" result}) - (assert-false (or - (get result "localvar in locals") (get result "localvar in globals") - (get result "globalvar in locals") (get result "globalvar in globals"))) - (assert-true (and - (get result "x in locals") (get result "x in globals"))) - (assert-false (or - (get result "y in locals") (get result "y in globals"))) - - (setv result {}) - (exec code {"x" 1 "result" result} {"y" 1}) - (assert-false (or - (get result "localvar in locals") (get result "localvar in globals") - (get result "globalvar in locals") (get result "globalvar in globals"))) - (assert-false (get result "x in locals")) - (assert-true (get result "x in globals")) - (assert-true (get result "y in locals")) - (assert-false (get result "y in globals"))) - -(defn test-filter [] - (setv res (list (filter (fn [x] (> x 0)) [ 1 2 3 -4 5]))) - (assert-equal res [ 1 2 3 5 ]) - ;; test with iter - (setv res (list (filter (fn [x] (> x 0)) (iter [ 1 2 3 -4 5 -6])))) - (assert-equal res [ 1 2 3 5]) - (setv res (list (filter (fn [x] (< x 0)) [ -1 -4 5 3 4]))) - (assert-false (= res [1 2])) - ;; test with empty list - (setv res (list (filter (fn [x] (< x 0)) []))) - (assert-equal res []) - ;; test with None in the list - (setv res (list - (filter (fn [x] (not (% x 2))) - (filter (fn [x] (isinstance x int)) - [1 2 None 3 4 None 4 6])))) - (assert-equal res [2 4 4 6]) - (setv res (list (filter (fn [x] (is x None)) [1 2 None 3 4 None 4 6]))) - (assert-equal res [None None])) - -(defn test-gensym [] - (setv s1 (hy.gensym)) - (assert (isinstance s1 hy.models.Symbol)) - (assert (= 0 (.find s1 "_G\uffff"))) - (setv s2 (hy.gensym "xx")) - (setv s3 (hy.gensym "xx")) - (assert (= 0 (.find s2 "_xx\uffff"))) - (assert (not (= s2 s3))) - (assert (not (= (str s2) (str s3))))) - -(defn test-import-init-hy [] - (import tests.resources.bin) - (assert (in "_null_fn_for_import_test" (dir tests.resources.bin)))) - -(defreader some-tag - "Some tag macro" - '1) - -(defn test-doc [capsys] - ;; https://github.com/hylang/hy/issues/1970 - ;; Let's first make sure we can doc the builtin macros - ;; before we create the user macros. - (doc doc) - (setv [out err] (.readouterr capsys)) - (assert (in "Gets help for a macro function" out)) - - (doc "#some-tag") - (setv [out err] (.readouterr capsys)) - (assert (in "Some tag macro" out)) - - (defmacro <-mangle-> [] - "a fancy docstring" - '(+ 2 2)) - (doc <-mangle->) - (setv [out err] (.readouterr capsys)) - ;; https://github.com/hylang/hy/issues/1946 - (assert (.startswith (.strip out) - f"Help on function {(hy.mangle '<-mangle->)} in module ")) - (assert (in "a fancy docstring" out)) - (assert (not err)) - - ;; make sure doc raises an error instead of - ;; presenting a default value help screen - (with [(pytest.raises NameError)] - (doc does-not-exist))) diff --git a/tests/native_tests/decorators.hy b/tests/native_tests/decorators.hy index 4b79d1f4d..3fedea468 100644 --- a/tests/native_tests/decorators.hy +++ b/tests/native_tests/decorators.hy @@ -1,3 +1,8 @@ +(import + asyncio + tests.resources [async-test]) + + (defn test-decorated-1line-function [] (defn foodec [func] (fn [] (+ (func) 1))) @@ -46,3 +51,12 @@ (.append l "bar body") arg) ; Body (.append l (bar)) (assert (= l ["dec" "arg" "foo" "foo fn" "bar body" 1]))) + + +(defn [async-test] test-decorated-defn/a [] + (defn decorator [func] (fn/a [] (/ (await (func)) 2))) + + (defn/a [decorator] coro-test [] + (await (asyncio.sleep 0)) + 42) + (assert (= (asyncio.run (coro-test)) 21))) diff --git a/tests/native_tests/defclass.hy b/tests/native_tests/defclass.hy index ef9b4f29d..f26e38d11 100644 --- a/tests/native_tests/defclass.hy +++ b/tests/native_tests/defclass.hy @@ -112,3 +112,47 @@ (set-sentinel)) (assert set-sentinel.set)) + + +(defn test-pep-3115 [] + (defclass member-table [dict] + (defn __init__ [self] + (setv self.member-names [])) + + (defn __setitem__ [self key value] + (when (not-in key self) + (.append self.member-names key)) + (dict.__setitem__ self key value))) + + (defclass OrderedClass [type] + (setv __prepare__ (classmethod (fn [metacls name bases] + (member-table)))) + + (defn __new__ [cls name bases classdict] + (setv result (type.__new__ cls name bases (dict classdict))) + (setv result.member-names classdict.member-names) + result)) + + (defclass MyClass [:metaclass OrderedClass] + (defn method1 [self] (pass)) + (defn method2 [self] (pass))) + + (assert (= (. (MyClass) member-names) + ["__module__" "__qualname__" "method1" "method2"]))) + + +(defn test-pep-487 [] + (defclass QuestBase [] + (defn __init-subclass__ [cls swallow #** kwargs] + (setv cls.swallow swallow))) + + (defclass Quest [QuestBase :swallow "african"]) + (assert (= (. (Quest) swallow) "african"))) + + +(do-mac (when hy._compat.PY3_12 '(defn test-type-params [] + (import tests.resources.tp :as ttp) + (defclass :tp [#^ int A #** B] C) + (assert (= (ttp.show C) [ + [ttp.TypeVar "A" int #()] + [ttp.ParamSpec "B" None #()]]))))) diff --git a/tests/native_tests/deftype.hy b/tests/native_tests/deftype.hy new file mode 100644 index 000000000..3fb6e0e25 --- /dev/null +++ b/tests/native_tests/deftype.hy @@ -0,0 +1,21 @@ +(do-mac (when hy._compat.PY3_12 '(do + +(import tests.resources.tp :as ttp) + + +(defn test-deftype [] + + (deftype Foo int) + (assert (is (type Foo) ttp.TypeAliasType)) + (assert (= Foo.__value__) int) + + (deftype Foo (| int bool)) + (assert (is (type Foo.__value__ hy.I.types.UnionType))) + + (deftype :tp [#^ int A #** B] Foo int) + (assert (= (ttp.show Foo) [ + [ttp.TypeVar "A" int #()] + [ttp.ParamSpec "B" None #()]]))))) + + +) diff --git a/tests/native_tests/del.hy b/tests/native_tests/del.hy new file mode 100644 index 000000000..6dc927db9 --- /dev/null +++ b/tests/native_tests/del.hy @@ -0,0 +1,16 @@ +(import + pytest) + + +(defn test-del [] + (setv foo 42) + (assert (= foo 42)) + (del foo) + (with [(pytest.raises NameError)] + foo) + (setv test (list (range 5))) + (del (get test 4)) + (assert (= test [0 1 2 3])) + (del (get test 2)) + (assert (= test [0 1 3])) + (assert (= (del) None))) diff --git a/tests/native_tests/do.hy b/tests/native_tests/do.hy new file mode 100644 index 000000000..05cd0d4e5 --- /dev/null +++ b/tests/native_tests/do.hy @@ -0,0 +1,12 @@ +(defn test-empty [] + (assert (is (do) None)) + (assert (is (if True (do) (do)) None))) + + +(defn test-nonempty [] + (assert (= (do 1 2 3) 3)) + (assert (= (do 3 2 1) 1)) + + (setv x "a") + (assert (= (do (setv x "b") "c") "c")) + (assert (= x "b"))) diff --git a/tests/native_tests/dots.hy b/tests/native_tests/dots.hy new file mode 100644 index 000000000..ef3ae3c66 --- /dev/null +++ b/tests/native_tests/dots.hy @@ -0,0 +1,79 @@ +(import + os) + + +(defn test-dotted-identifiers [] + (assert (= (.join " " ["one" "two"]) "one two")) + + (defclass X [object] []) + (defclass M [object] + (defn meth [self #* args #** kwargs] + (.join " " (+ #("meth") args + (tuple (map (fn [k] (get kwargs k)) (sorted (.keys kwargs)))))))) + + (setv x (X)) + (setv m (M)) + + (assert (= (.meth m) "meth")) + (assert (= (.meth m "foo" "bar") "meth foo bar")) + (assert (= (.meth :b "1" :a "2" m "foo" "bar") "meth foo bar 2 1")) + (assert (= (.meth m #* ["foo" "bar"]) "meth foo bar")) + + (setv x.p m) + (assert (= (.p.meth x) "meth")) + (assert (= (.p.meth x "foo" "bar") "meth foo bar")) + (assert (= (.p.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) + (assert (= (.p.meth x #* ["foo" "bar"]) "meth foo bar")) + + (setv x.a (X)) + (setv x.a.b m) + (assert (= (.a.b.meth x) "meth")) + (assert (= (.a.b.meth x "foo" "bar") "meth foo bar")) + (assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) + (assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar")) + + (assert (= (.__str__ :foo) ":foo"))) + + +(defn test-dot-macro [] + (defclass mycls [object]) + + (setv foo [(mycls) (mycls) (mycls)]) + (assert (is (. foo) foo)) + (assert (is (. foo [0]) (get foo 0))) + (assert (is (. foo [0] __class__) mycls)) + (assert (is (. foo [1] __class__) mycls)) + (assert (is (. foo [(+ 1 1)] __class__) mycls)) + (assert (= (. foo [(+ 1 1)] __class__ __name__ [0]) "m")) + (assert (= (. foo [(+ 1 1)] __class__ __name__ [1]) "y")) + (assert (= (. os (getcwd) (isalpha) __class__ __name__ [0]) "b")) + (assert (= (. "ab hello" (strip "ab ") (upper)) "HELLO")) + (assert (= (. "hElLO\twoRld" (expandtabs :tabsize 4) (lower)) "hello world")) + + (setv bar (mycls)) + (setv (. foo [1]) bar) + (assert (is bar (get foo 1))) + (setv (. foo [1] test) "hello") + (assert (= (getattr (. foo [1]) "test") "hello"))) + + +(defn test-multidot [] + (setv a 1 b 2 c 3) + + (defn .. [#* args] + (.join "~" (map str args))) + (assert (= ..a.b.c "None~1~2~3")) + + (defmacro .... [#* args] + (.join "@" (map str args))) + (assert (= ....uno.dos.tres "None@uno@dos@tres"))) + + +(defn test-ellipsis [] + (global Ellipsis) + (assert (is ... Ellipsis)) + (setv e Ellipsis) + (setv Ellipsis 14) + (assert (= Ellipsis 14)) + (assert (!= ... 14)) + (assert (is ... e))) diff --git a/tests/native_tests/eval_foo_compile.hy b/tests/native_tests/eval_foo_compile.hy new file mode 100644 index 000000000..cdd0e293f --- /dev/null +++ b/tests/native_tests/eval_foo_compile.hy @@ -0,0 +1,47 @@ +;; Tests of `eval-when-compile`, `eval-and-compile`, and `do-mac` + + +(defn test-eval-foo-compile-return-values [] + (eval-and-compile (setv jim 0)) + + (setv derrick (eval-and-compile (+= jim 1) 2)) + (assert (= jim 1)) + (assert (= derrick 2)) + + (setv derrick (eval-and-compile)) + (assert (is derrick None)) + + (setv derrick 3) + (setv derrick (eval-when-compile (+= jim 1) 2)) + (assert (= jim 1)) + (assert (is derrick None))) + + +(defn test-do-mac [] + + (assert (is (do-mac) None)) + + (setv x 2) + (setv x-compile-time (do-mac + (setv x 3) + x)) + (assert (= x 2)) + (assert (= x-compile-time 3)) + + (eval-when-compile (setv x 4)) + (assert (= x 2)) + (assert (= (do-mac x) 4)) + + (defmacro m [] + (global x) + (setv x 5)) + (m) + (assert (= x 2)) + (assert (= (do-mac x) 5)) + + (setv l []) + (do-mac `(do ~@(* ['(.append l 1)] 5))) + (assert (= l [1 1 1 1 1])) + + (do-mac `(setv ~(hy.models.Symbol (* "x" 5)) "foo")) + (assert (= xxxxx "foo"))) diff --git a/tests/native_tests/functions.hy b/tests/native_tests/functions.hy new file mode 100644 index 000000000..fd1d31b61 --- /dev/null +++ b/tests/native_tests/functions.hy @@ -0,0 +1,357 @@ +;; Tests of `fn`, `defn`, `return`, and `yield` + +(import + asyncio + typing [List] + pytest + tests.resources [async-test]) + + +(defn test-fn [] + (setv square (fn [x] (* x x))) + (assert (= 4 (square 2))) + (setv lambda_list (fn [test #* args] #(test args))) + (assert (= #(1 #(2 3)) (lambda_list 1 2 3)))) + + +(defn test-immediately-call-lambda [] + (assert (= 2 ((fn [] (+ 1 1)))))) + + +(defn test-fn-return [] + (setv fn-test ((fn [] (fn [] (+ 1 1))))) + (assert (= (fn-test) 2)) + (setv fn-test (fn [])) + (assert (= (fn-test) None))) + + +(defn [async-test] test-fn/a [] + (assert (= (asyncio.run ((fn/a [] (await (asyncio.sleep 0)) [1 2 3]))) + [1 2 3]))) + + +(defn test-defn-evaluation-order [] + (setv acc []) + (defn my-fun [] + (.append acc "Foo") + (.append acc "Bar") + (.append acc "Baz")) + (my-fun) + (assert (= acc ["Foo" "Bar" "Baz"]))) + + +(defn test-defn-return [] + (defn my-fun [x] + (+ x 1)) + (assert (= 43 (my-fun 42)))) + + +(defn test-defn-lambdakey [] + "Test defn with a `&symbol` function name." + (defn &hy [] 1) + (assert (= (&hy) 1))) + + +(defn test-defn-evaluation-order-with-do [] + (setv acc []) + (defn my-fun [] + (do + (.append acc "Foo") + (.append acc "Bar") + (.append acc "Baz"))) + (my-fun) + (assert (= acc ["Foo" "Bar" "Baz"]))) + + +(defn test-defn-do-return [] + (defn my-fun [x] + (do + (+ x 42) ; noop + (+ x 1))) + (assert (= 43 (my-fun 42)))) + + +(defn test-defn-dunder-name [] + "`defn` should preserve `__name__`." + + (defn phooey [x] + (+ x 1)) + (assert (= phooey.__name__ "phooey")) + + (defn mooey [x] + (+= x 1) + x) + (assert (= mooey.__name__ "mooey"))) + + +(defn test-defn-annotations [] + + (defn #^ int f [#^ (get List int) p1 p2 #^ str p3 #^ str [o1 None] #^ int [o2 0] + #^ str #* rest #^ str k1 #^ int [k2 0] #^ bool #** kwargs]) + + (assert (is (. f __annotations__ ["return"]) int)) + (for [[k v] (.items (dict + :p1 (get List int) :p3 str :o1 str :o2 int + :k1 str :k2 int :kwargs bool))] + (assert (= (. f __annotations__ [k]) v)))) + + +(do-mac (when hy._compat.PY3_12 '(defn test-type-params [] + (import tests.resources.tp :as ttp) + + (defn foo []) + (assert (= (ttp.show foo) [])) + + ; `defn` with a type parameter + (defn :tp [T] #^ T foo [#^ T x] + (+ x 1)) + (assert (= (foo 3) 4)) + (assert (= (ttp.show foo) [[ttp.TypeVar "T" None #()]])) + + ; `fn` with a type parameter + (setv foo (fn :tp [T] #^ T [#^ T x] + (+ x 2))) + (assert (= (foo 3) 5)) + (assert (= (ttp.show foo) [[ttp.TypeVar "T" None #()]])) + + ; Bounds and constraints + (defn :tp [#^ int T] foo []) + (assert (= (ttp.show foo) [[ttp.TypeVar "T" int #()]])) + (defn :tp [#^ #(int str) T] foo []) + (assert (= (ttp.show foo) [[ttp.TypeVar "T" None #(int str)]])) + + ; `TypeVarTuple`s and `ParamSpec`s + (defn :tp [#* T] foo []) + (assert (= (ttp.show foo) [[ttp.TypeVarTuple "T" None #()]])) + (defn :tp [#** T] foo []) + (assert (= (ttp.show foo) [[ttp.ParamSpec "T" None #()]])) + + ; A more complex case + (defn + :tp [A #^ int B #* C #** D #* E #^ #(bool float) F] + foo []) + (assert (= (ttp.show foo) [ + [ttp.TypeVar "A" None #()] + [ttp.TypeVar "B" int #()] + [ttp.TypeVarTuple "C" None #()] + [ttp.ParamSpec "D" None #()] + [ttp.TypeVarTuple "E" None #()] + [ttp.TypeVar "F" None #(bool float)]])) + + ; Illegal attempts to annotate unpacking + (with [(pytest.raises hy.errors.HySyntaxError)] + (hy.eval '(defn :tp [#^ int #* T] foo []))) + (with [(pytest.raises hy.errors.HySyntaxError)] + (hy.eval '(defn :tp [#^ #(int str) #** T] foo [])))))) + + +(defn test-lambda-keyword-lists [] + (defn foo [x #* xs #** kw] [x xs kw]) + (assert (= (foo 10 20 30) [10 #(20 30) {}]))) + + +(defn test-optional-arguments [] + (defn foo [a b [c None] [d 42]] [a b c d]) + (assert (= (foo 1 2) [1 2 None 42])) + (assert (= (foo 1 2 3) [1 2 3 42])) + (assert (= (foo 1 2 3 4) [1 2 3 4]))) + + +(defn test-kwonly [] + ;; keyword-only with default works + (defn kwonly-foo-default-false [* [foo False]] foo) + (assert (= (kwonly-foo-default-false) False)) + (assert (= (kwonly-foo-default-false :foo True) True)) + ;; keyword-only without default ... + (defn kwonly-foo-no-default [* foo] foo) + (with [e (pytest.raises TypeError)] + (kwonly-foo-no-default)) + (assert (in "missing 1 required keyword-only argument: 'foo'" + (. e value args [0]))) + ;; works + (assert (= (kwonly-foo-no-default :foo "quux") "quux")) + ;; keyword-only with other arg types works + (defn function-of-various-args [a b #* args foo #** kwargs] + #(a b args foo kwargs)) + (assert (= (function-of-various-args 1 2 3 4 :foo 5 :bar 6 :quux 7) + #(1 2 #(3 4) 5 {"bar" 6 "quux" 7})))) + + +(defn test-only-parse-lambda-list-in-defn [] + (with [(pytest.raises NameError)] + (setv x [#* spam] y 1))) + + +(defn [async-test] test-defn/a [] + (defn/a coro-test [] + (await (asyncio.sleep 0)) + [1 2 3]) + (assert (= (asyncio.run (coro-test)) [1 2 3]))) + + +(defn [async-test] test-no-async-gen-return [] + ; https://github.com/hylang/hy/issues/2523 + (defn/a runner [gen] + (setv vals []) + (for [:async val (gen)] + (.append vals val)) + vals) + (defn/a naysayer [] + (yield "nope")) + (assert (= (asyncio.run (runner naysayer)) ["nope"])) + (assert (= (asyncio.run (runner (fn/a [] (yield "dope!")) ["dope!"]))))) + + +(defn test-root-set-correctly [] + ; https://github.com/hylang/hy/issues/2475 + ((. defn) not-async [] "ok") + (assert (= (not-async) "ok")) + (require builtins) + (builtins.defn also-not-async [] "ok") + (assert (= (also-not-async) "ok"))) + + +(defn test-return [] + + ; `return` in main line + (defn f [x] + (return (+ x "a")) + (+ x "b")) + (assert (= (f "q") "qa")) + + ; Nullary `return` + (defn f [x] + (return) + 5) + (assert (is (f "q") None)) + + ; `return` in `when` + (defn f [x] + (when (< x 3) + (return [x 1])) + [x 2]) + (assert (= (f 2) [2 1])) + (assert (= (f 4) [4 2])) + + ; `return` in a loop + (setv accum []) + (defn f [x] + (while True + (when (= x 0) + (return)) + (.append accum x) + (-= x 1)) + (.append accum "this should never be appended") + 1) + (assert (is (f 5) None)) + (assert (= accum [5 4 3 2 1])) + + ; `return` of a `do` + (setv accum []) + (defn f [] + (return (do + (.append accum 1) + 3)) + 4) + (assert (= (f) 3)) + (assert (= accum [1])) + + ; `return` of an `if` that will need to be compiled to a statement + (setv accum []) + (defn f [x] + (return (if (= x 1) + (do + (.append accum 1) + "a") + (do + (.append accum 2) + "b"))) + "c") + (assert (= (f 2) "b")) + (assert (= accum [2]))) + + +(defn test-yield-from [] + (defn yield-from-test [] + (for [i (range 3)] + (yield i)) + (yield-from [1 2 3])) + (assert (= (list (yield-from-test)) [0 1 2 1 2 3]))) + + +(defn test-yield-from-exception-handling [] + (defn yield-from-subgenerator-test [] + (yield 1) + (yield 2) + (yield 3) + (/ 1 0)) + (defn yield-from-test [] + (for [i (range 3)] + (yield i)) + (try + (yield-from (yield-from-subgenerator-test)) + (except [e ZeroDivisionError] + (yield 4)))) + (assert (= (list (yield-from-test)) [0 1 2 1 2 3 4]))) + + +(defn test-yield [] + (defn gen [] (for [x [1 2 3 4]] (yield x))) + (setv ret 0) + (for [y (gen)] (setv ret (+ ret y))) + (assert (= ret 10))) + + +(defn test-yield-with-return [] + (defn gen [] (yield 3) "goodbye") + (setv gg (gen)) + (assert (= 3 (next gg))) + (with [e (pytest.raises StopIteration)] + (next gg)) + (assert (= e.value.value "goodbye"))) + + +(defn test-yield-in-try [] + (setv hit-finally False) + (defn gen [] + (setv x 1) + (try + (yield x) + (finally + (nonlocal hit-finally) + (setv hit-finally True)))) + (setv output (list (gen))) + (assert (= [1] output)) + (assert hit-finally)) + + +(defn test-midtree-yield [] + "Test yielding with a returnable." + (defn kruft [] (yield) (+ 1 1))) + + +(defn test-midtree-yield-in-for [] + "Test yielding in a for with a return." + (defn kruft-in-for [] + (for [i (range 5)] + (yield i)) + (+ 1 2))) + + +(defn test-midtree-yield-in-while [] + "Test yielding in a while with a return." + (defn kruft-in-while [] + (setv i 0) + (while (< i 5) + (yield i) + (setv i (+ i 1))) + (+ 2 3))) + + +(defn test-multi-yield [] + (defn multi-yield [] + (for [i (range 3)] + (yield i)) + (yield "a") + (yield "end")) + (assert (= (list (multi-yield)) [0 1 2 "a" "end"]))) diff --git a/tests/native_tests/hy_eval.hy b/tests/native_tests/hy_eval.hy new file mode 100644 index 000000000..c69267432 --- /dev/null +++ b/tests/native_tests/hy_eval.hy @@ -0,0 +1,260 @@ +"Tests of the user-facing function `hy.eval`." + + +(import + re + pytest) + + +(defn test-eval [] + (assert (= 2 (hy.eval (quote (+ 1 1))))) + (setv x 2) + (assert (= 4 (hy.eval (quote (+ x 2))))) + (setv test-payload (quote (+ x 2))) + (setv x 4) + (assert (= 6 (hy.eval test-payload))) + (assert (= 9 ((hy.eval (quote (fn [x] (+ 3 3 x)))) 3))) + (assert (= 1 (hy.eval (quote 1)))) + (assert (= "foobar" (hy.eval (quote "foobar")))) + (setv x (quote 42)) + (assert (= 42 (hy.eval x))) + (assert (= 27 (hy.eval (+ (quote (*)) (* [(quote 3)] 3))))) + (assert (= None (hy.eval (quote (print ""))))) + + ;; https://github.com/hylang/hy/issues/1041 + (assert (is (hy.eval 're) re)) + (assert (is ((fn [] (hy.eval 're))) re))) + + +(defn test-eval-false [] + (assert (is (hy.eval 'False) False)) + (assert (is (hy.eval 'None) None)) + (assert (= (hy.eval '0) 0)) + (assert (= (hy.eval '"") "")) + (assert (= (hy.eval 'b"") b"")) + (assert (= (hy.eval ':) :)) + (assert (= (hy.eval '[]) [])) + (assert (= (hy.eval '#()) #())) + (assert (= (hy.eval '{}) {})) + (assert (= (hy.eval '#{}) #{}))) + + +(defn test-eval-quasiquote [] + ; https://github.com/hylang/hy/issues/1174 + + (for [x [ + None False True + 5 5.1 + 5j 5.1j 2+1j 1.2+3.4j + "" b"" + "apple bloom" b"apple bloom" "⚘" b"\x00" + [] #{} {} + [1 2 3] #{1 2 3} {"a" 1 "b" 2}]] + (assert (= (hy.eval `(get [~x] 0)) x)) + (assert (= (hy.eval x) x))) + + (setv kw :mykeyword) + (assert (= (get (hy.eval `[~kw]) 0) kw)) + (assert (= (hy.eval kw) kw)) + + (assert (= (hy.eval #()) #())) + (assert (= (hy.eval #(1 2 3)) #(1 2 3))) + + (assert (= (hy.eval `(+ "a" ~(+ "b" "c"))) "abc")) + + (setv l ["a" "b"]) + (setv n 1) + (assert (= (hy.eval `(get ~l ~n) "b"))) + + (setv d {"a" 1 "b" 2}) + (setv k "b") + (assert (= (hy.eval `(get ~d ~k)) 2))) + + +(setv outer "O") + +(defn test-globals [] + (assert (= (hy.eval 'foo {"foo" 2}) 2)) + (with [(pytest.raises NameError)] + (hy.eval 'foo {})) + + (assert (= outer "O")) + (assert (= (hy.eval 'outer) "O")) + (with [(pytest.raises NameError)] + (hy.eval 'outer {})) + + (hy.eval '(do + (global outer) + (setv outer "O2"))) + (assert (= outer "O2")) + + (hy.eval :globals {"outer" "I"} '(do + (global outer) + (setv outer "O3"))) + (assert (= outer "O2")) + + ; If `globals` is provided but not `locals`, then `globals` + ; substitutes in for `locals`. + (defn try-it [#** eval-args] + (setv d (dict :g1 1 :g2 2)) + (hy.eval :globals d #** eval-args '(do + (global g2 g3) + (setv g2 "newv" g3 3 l 4))) + (del (get d "__builtins__")) + d) + (setv ls {}) + (assert (= (try-it) (dict :g1 1 :g2 "newv" :g3 3 :l 4))) + (assert (= (try-it :locals ls) (dict :g1 1 :g2 "newv" :g3 3))) + (assert (= ls {"l" 4})) + + ; If `module` is provided but `globals` isn't, the dictionary of + ; `module` is used for globals. If `locals` also isn't provided, + ; the same dictionary is used for that, too. + (import string) + (assert (= + (hy.eval 'digits :module string) + "0123456789")) + (assert (= + (hy.eval 'digits :module "string") + "0123456789")) + (assert (= + (hy.eval 'digits :module string :globals {"digits" "boo"}) + "boo")) + (with [(pytest.raises NameError)] + (hy.eval 'digits :module string :globals {})) + (hy.eval :module string '(do + (global hytest-string-g) + (setv hytest-string-g "hi") + (setv hytest-string-l "bye"))) + (assert (= string.hytest-string-g "hi")) + (assert (= string.hytest-string-l "bye"))) + + +(defn test-locals [] + (assert (= (hy.eval 'foo :locals {"foo" 2}) 2)) + (with [(pytest.raises NameError)] + (hy.eval 'foo :locals {})) + + (setv d (dict :l1 1 :l2 2 :hippopotamus "local_v")) + (hy.eval :locals d '(do + (global hippopotamus) + (setv l2 "newv" l3 3 hippopotamus "global_v"))) + (assert (= d (dict :l1 1 :l2 "newv" :l3 3 :hippopotamus "local_v"))) + (assert (= (get (globals) "hippopotamus") "global_v")) + (assert (= hippopotamus "global_v")) + + ; `hy` is implicitly available even when `locals` and `globals` are + ; provided. + (assert (= + (hy.eval :locals {"foo" "A"} :globals {"bar" "B"} + '(hy.repr (+ foo bar))) + #[["AB"]])) + ; Even though `hy.eval` deletes the `hy` implicitly added to + ; `locals`, references in returned code still work. + (setv d {"a" 1}) + (setv f (hy.eval '(fn [] (hy.repr "hello")) :locals d)) + (assert (= d {"a" 1})) + (assert (= (f) #[["hello"]]))) + + +(defn test-globals-and-locals [] + (setv gd (dict :g1 "apple" :g2 "banana")) + (setv ld (dict :l1 "Austin" :l2 "Boston")) + (hy.eval :globals gd :locals ld '(do + (global g2 g3) + (setv g2 "newg-val" g3 "newg-var" l2 "newl-val" l3 "newl-var"))) + (del (get gd "__builtins__")) + (assert (= gd (dict :g1 "apple" :g2 "newg-val" :g3 "newg-var"))) + (assert (= ld (dict :l1 "Austin" :l2 "newl-val" :l3 "newl-var")))) + + +(defn test-no-extra-hy-removal [] + "`hy.eval` shouldn't remove `hy` from a provided namespace if it + was already there." + (setv g {}) + (exec "import hy" g) + (assert (= (hy.eval '(hy.repr [1 2]) g) "[1 2]")) + (assert (in "hy" g))) + + +(defmacro test-macro [] + '(setv blah "test from here")) +(defmacro cheese [] + "gorgonzola") + +(defn test-macros [] + (setv M "tests.resources.macros") + + ; Macros defined in `module` can be called. + (assert (= (hy.eval '(do (test-macro) blah)) "test from here")) + (assert (= (hy.eval '(do (test-macro) blah) :module M) 1)) + + ; `defmacro` creates a new macro in the module. + (hy.eval '(defmacro bilb-ono [] "creative consulting") :module M) + (assert (= (hy.eval '(bilb-ono) :module M) "creative consulting")) + (with [(pytest.raises NameError)] + (hy.eval '(bilb-ono))) + + ; When `module` is provided, implicit access to macros in the + ; current scope is lost. + (assert (= (hy.eval '(cheese)) "gorgonzola")) + (with [(pytest.raises NameError)] + (hy.eval '(cheese) :module M)) + + ; You can still use `require` inside `hy.eval`. + (hy.eval '(require tests.resources.tlib [qplah])) + (assert (= (hy.eval '(qplah 1)) [8 1]))) + + +(defn test-extra-macros [] + (setv ab 15) + + (assert (= + (hy.eval '(chippy a b) :macros (dict + :chippy (fn [arg1 arg2] + (hy.models.Symbol (+ (str arg1) (str arg2)))))) + 15)) + + ; By default, `hy.eval` can't see local macros. + (defmacro oh-hungee [arg1 arg2] + (hy.models.Symbol (+ (str arg1) (str arg2)))) + (with [(pytest.raises NameError)] + (hy.eval '(oh-hungee a b))) + ; But you can pass them in with the `macros` argument. + (assert (= + (hy.eval '(oh-hungee a b) :macros (local-macros)) + 15)) + (assert (= + (hy.eval '(oh-hungee a b) :macros {"oh_hungee" (get-macro oh-hungee)} + 15))) + + ; You can shadow a global macro. + (assert (= + (hy.eval '(cheese)) + "gorgonzola")) + (assert (= + (hy.eval '(cheese) :macros {"cheese" (fn [] "cheddar")}) + "cheddar")) + + ; Or even a core macro, and with no warning. + (assert (= + (hy.eval '(+ 1 1) :macros + {(hy.mangle "+") (fn [#* args] + (.join "" (gfor x args (str (int x)))))}) + "11"))) + + +(defn test-filename [] + (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.I.traceback.format-tb e.tb) -1)))) + + +(defn test-eval-failure [] + ; yo dawg + (with [(pytest.raises TypeError)] (hy.eval '(hy.eval))) + (defclass C) + (with [(pytest.raises TypeError)] (hy.eval (C))) + (with [(pytest.raises TypeError)] (hy.eval 'False [])) + (with [(pytest.raises TypeError)] (hy.eval 'False {} 1))) diff --git a/tests/native_tests/hy_misc.hy b/tests/native_tests/hy_misc.hy new file mode 100644 index 000000000..a75d63c20 --- /dev/null +++ b/tests/native_tests/hy_misc.hy @@ -0,0 +1,183 @@ +;; Tests of `hy.gensym`, `hy.macroexpand`, `hy.macroexpand-1`, +;; `hy.disassemble`, `hy.read`, `hy.I`, and `hy.R` + +(import + pytest) + + +(defn test-gensym [] + (setv s1 (hy.gensym)) + (assert (isinstance s1 hy.models.Symbol)) + (assert (.startswith s1 "_hy_gensym__")) + (setv s2 (hy.gensym "xx")) + (setv s3 (hy.gensym "xx")) + (assert (.startswith s2 "_hy_gensym_xx_")) + (assert (!= s2 s3)) + (assert (!= (str s2) (str s3))) + (assert (.startswith (hy.gensym "•ab") "_hy_gensym_XbulletXab_"))) + + +(defmacro mac [x expr] + `(~@expr ~x)) + + +(defn test-macroexpand [] + (assert (= + (hy.macroexpand '(mac (a b) (x y))) + '(x y (a b)))) + (assert (= + (hy.macroexpand '(mac (a b) (mac 5))) + '(a b 5))) + (assert (= + (hy.macroexpand '(qplah "phooey") :module hy.I.tests.resources.tlib) + '[8 "phooey"])) + (assert (= + (hy.macroexpand '(chippy 1) :macros + {"chippy" (fn [x] `[~x ~x])}) + '[1 1])) + ; Non-Expressions just get returned as-is. + (defn f []) + (assert (is + (hy.macroexpand f) + f)) + ; Likewise Expressions that aren't macro calls. + (setv model '(wmbatt 1 2)) + (assert (is + (hy.macroexpand model) + model)) + ; If the macro expands to a `Result`, the user gets the original + ; back instead of the `Result`. + (setv model '(+ 1 1)) + (assert (is + (hy.macroexpand model) + model))) + + +(defmacro m-with-named-import [] + (import math [pow]) + (pow 2 3)) + +(defn test-macroexpand-with-named-import [] + ; https://github.com/hylang/hy/issues/1207 + (assert (= (hy.macroexpand '(m-with-named-import)) (hy.models.Float (** 2 3))))) + + +(defn test-macroexpand-1 [] + (assert (= (hy.macroexpand-1 '(mac (a b) (mac 5))) + '(mac 5 (a b))))) + + +(defn test-disassemble [] + (import re) + (defn nos [x] (re.sub r"\s" "" x)) + (assert (= (nos (hy.disassemble '(do (leaky) (leaky) (macros)))) + (nos + "Module( + body=[Expr(value=Call(func=Name(id='leaky', ctx=Load()), args=[], keywords=[])), + Expr(value=Call(func=Name(id='leaky', ctx=Load()), args=[], keywords=[])), + Expr(value=Call(func=Name(id='macros', ctx=Load()), args=[], keywords=[]))],type_ignores=[])"))) + (assert (= (nos (hy.disassemble '(do (leaky) (leaky) (macros)) True)) + "leaky()leaky()macros()")) + (assert (= (re.sub r"[()\n ]" "" (hy.disassemble `(+ ~(+ 1 1) 40) True)) + "2+40"))) + + +(defn test-read-file-object [] + (import io [StringIO]) + + (setv stdin-buffer (StringIO "(+ 2 2)\n(- 2 2)")) + (assert (= (hy.eval (hy.read stdin-buffer)) 4)) + (assert (isinstance (hy.read stdin-buffer) hy.models.Expression)) + + ; Multiline test + (setv stdin-buffer (StringIO "(\n+\n41\n1\n)\n(-\n2\n1\n)")) + (assert (= (hy.eval (hy.read stdin-buffer)) 42)) + (assert (= (hy.eval (hy.read stdin-buffer)) 1)) + + ; EOF test + (setv stdin-buffer (StringIO "(+ 2 2)")) + (hy.read stdin-buffer) + (with [(pytest.raises EOFError)] + (hy.read stdin-buffer))) + + +(defn test-read-str [] + (assert (= (hy.read "(print 1)") '(print 1))) + (assert (is (type (hy.read "(print 1)")) (type '(print 1)))) + + ; Watch out for false values: https://github.com/hylang/hy/issues/1243 + (assert (= (hy.read "\"\"") '"")) + (assert (is (type (hy.read "\"\"")) (type '""))) + (assert (= (hy.read "[]") '[])) + (assert (is (type (hy.read "[]")) (type '[]))) + (assert (= (hy.read "0") '0)) + (assert (is (type (hy.read "0")) (type '0)))) + + +(defn test-hyI [] + (defmacro no-name [name] + `(with [(pytest.raises NameError)] ~name)) + + ; `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.I.math.sqrt 4) 2)) + (assert (= math.sqrt "hello")) + + ; It still works in a macro expansion. + (defmacro frac [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.I.os/path.basename "foo/bar") "bar")) + (no-name os) + (no-name path) + + ; `hy.I.__getattr__` attempts to cope with mangling. + (with [e (pytest.raises ModuleNotFoundError)] + (hy.I.a-b☘c-d/e.z)) + (assert (= e.value.name (hy.mangle "a-b☘c-d"))) + ; `hy.I.__call__` doesn't. + (with [e (pytest.raises ModuleNotFoundError)] + (hy.I "a-b☘c-d/e.z")) + (assert (= e.value.name "a-b☘c-d/e"))) + + +(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. + + (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.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")))) diff --git a/tests/native_tests/hy_repr.hy b/tests/native_tests/hy_repr.hy index 0595d27ae..f113bcf98 100644 --- a/tests/native_tests/hy_repr.hy +++ b/tests/native_tests/hy_repr.hy @@ -13,9 +13,9 @@ (list o)) :setv x (.rstrip x) :if (and x (not (.startswith x ";"))) - x (if (.startswith x "!") - [(cut x 1 None) (+ "'" (cut x 1 None))] - [x]) + x (if (in (get x 0) "':") + [x] + [x (+ "'" x)]) x)] (setv rep (hy.repr (hy.eval (hy.read original-str)))) @@ -28,6 +28,7 @@ ; hy-reprs from the input syntax. (setv values [ + ':mykeyword {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3} 'f"the answer is {(+ 2 2) = }" @@ -40,8 +41,8 @@ (defn test-hy-repr-no-roundtrip [] ; Test one of the corner cases in which hy-repr doesn't - ; round-trip: when a Hy Object contains a non-Hy Object, we - ; promote the constituent to a Hy Object. + ; round-trip: when a Hy model contains a non-model, we + ; promote the constituent to a model. (setv orig `[a ~5.0]) (setv reprd (hy.repr orig)) diff --git a/tests/native_tests/import.hy b/tests/native_tests/import.hy new file mode 100644 index 000000000..e6846432b --- /dev/null +++ b/tests/native_tests/import.hy @@ -0,0 +1,201 @@ +;; Tests of `import`, `require`, and `export` + +(import + importlib + os.path + os.path [exists isdir isfile] + sys :as systest + sys + pytest + hy._compat [PYODIDE]) + + +(defn test-imported-bits [] + (assert (is (exists ".") True)) + (assert (is (isdir ".") True)) + (assert (is (isfile ".") False))) + + +(defn test-importas [] + (assert (!= (len systest.path) 0))) + + +(defn test-import-syntax [] + ;; Simple import + (import sys os) + + ;; from os.path import basename + (import os.path [basename]) + (assert (= (basename "/some/path") "path")) + + ;; import os.path as p + (import os.path :as p) + (assert (= p.basename basename)) + + ;; from os.path import basename as bn + (import os.path [basename :as bn]) + (assert (= bn basename)) + + ;; Multiple stuff to import + (import sys + os.path [dirname] + os.path :as op + os.path [dirname :as dn]) + (assert (= (dirname "/some/path") "/some")) + (assert (= op.dirname dirname)) + (assert (= dn dirname))) + + +(defn test-relative-import [] + (import ..resources [tlib in-init]) + (assert (= tlib.SECRET-MESSAGE "Hello World")) + (assert (= in-init "chippy")) + (import .. [resources]) + (assert (= resources.in-init "chippy"))) + + +(defn test-import-init-hy [] + (import tests.resources.bin) + (assert (in "_null_fn_for_import_test" (dir tests.resources.bin)))) + + +(require + tests.resources.tlib + tests.resources.tlib :as TL + tests.resources.tlib [qplah] + tests.resources.tlib [parald :as parald-alias] + tests.resources [tlib macros :as TM exports-none] + os [path]) + ; The last one is a no-op, since the module `os.path` exists but + ; contains no macros. + +(defn test-require-global [] + (assert (= (tests.resources.tlib.parald 1 2 3) [9 1 2 3])) + (assert (= (tests.resources.tlib.✈ "silly") "plane silly")) + (assert (= (tests.resources.tlib.hyx_XairplaneX "foolish") "plane foolish")) + (assert (is + (get-macro tests.resources.tlib.✈) + (get _hy_macros (hy.mangle "tests.resources.tlib.✈")))) + + (assert (= (TL.parald 1 2 3) [9 1 2 3])) + (assert (= (TL.✈ "silly") "plane silly")) + (assert (= (TL.hyx_XairplaneX "foolish") "plane foolish")) + + (assert (= (qplah 1 2 3) [8 1 2 3])) + + (assert (= (parald-alias 1 2 3) [9 1 2 3])) + + (assert (in "tlib.qplah" _hy_macros)) + (assert (in (hy.mangle "TM.test-macro") _hy_macros)) + (assert (in (hy.mangle "exports-none.cinco") _hy_macros)) + + (with [(pytest.raises NameError)] + (parald 1 2 3 4)) + + (with [(pytest.raises hy.errors.HyRequireError)] + (hy.eval '(require tests.resources [does-not-exist])))) + + +(require tests.resources.more-test-macros *) + +(defn test-require-global-star-without-exports [] + (assert (= (bairn 1 2 3) [14 1 2 3])) + (assert (= (cairn 1 2 3) [15 1 2 3])) + (with [(pytest.raises NameError)] + (_dairn 1 2 3 4))) + + +(require tests.resources.exports *) + +(defn test-require-global-star-with-exports [] + (assert (= (casey 1 2 3) [11 1 2 3])) + (assert (= (☘ 1 2 3) [13 1 2 3])) + (with [(pytest.raises NameError)] + (brother 1 2 3 4))) + + +(require + ..resources.macros [test-macro-2] + .beside [xyzzy] + . [beside :as BS]) + +(defn test-require-global-relative [] + (assert (in "test_macro_2" _hy_macros)) + (assert (in "xyzzy" _hy_macros)) + (assert (in "BS.xyzzy" _hy_macros))) + + +;; `remote-test-macro` is a macro used within +;; `tests.resources.macro-with-require.test-module-macro`. +;; Here, we introduce an equivalently named version that, when +;; used, will expand to a different output string. +(defmacro remote-test-macro [x] + "this is the home version of `remote-test-macro`!") + +(require tests.resources.macro-with-require *) +(defmacro home-test-macro [x] + (.format "This is the home version of `remote-test-macro` returning {}!" (int x))) + +(defn test-macro-namespace-resolution [] + "Confirm that new versions of macro-macro dependencies do not shadow the +versions from the macro's own module, but do resolve unbound macro references +in expansions." + + ;; Was the above macro created properly? + (assert (in "remote_test_macro" _hy_macros)) + + (setv remote-test-macro (get _hy_macros "remote_test_macro")) + + (setv module-name-var "tests.native_tests.native_macros.test-macro-namespace-resolution") + (assert (= (+ "This macro was created in tests.resources.macros, " + "expanded in tests.native_tests.native_macros.test-macro-namespace-resolution " + "and passed the value 2.") + (test-module-macro 2))) + + ;; Now, let's use a `require`d macro that depends on another macro defined only + ;; in this scope. + (assert (= "This is the home version of `remote-test-macro` returning 3!" + (test-module-macro-2 3)))) + + +(defn test-no-surprise-shadow [tmp-path monkeypatch] + "Check that an out-of-module macro doesn't shadow a function." + ; https://github.com/hylang/hy/issues/2451 + + (monkeypatch.syspath-prepend tmp-path) + (.write-text (/ tmp-path "wexter_a.hy") #[[ + (defmacro helper [] + "helper a (macro)") + (defmacro am [form] + form)]]) + (.write-text (/ tmp-path "wexter_b.hy") #[[ + (require wexter-a [am]) + (defn helper [] + "helper b (function)") + (setv v1 (helper)) + (setv v2 (am (helper)))]]) + + (import wexter-b) + (assert (= wexter-b.v1 "helper b (function)")) + (assert (= wexter-b.v2 "helper b (function)"))) + + +(defn test-recursive-require-star [] + "(require foo *) should pull in macros required by `foo`." + (require tests.resources.macro-with-require *) + + (test-macro) + (assert (= blah 1))) + + +(defn test-export-objects [] + ; We use `hy.eval` here because of a Python limitation that + ; importing `*` is only allowed at the module level. + (hy.eval '(do + (import tests.resources.exports *) + (assert (= (jan) 21)) + (assert (= (♥) 23)) + (with [(pytest.raises NameError)] + (wayne)) + (import tests.resources.exports [wayne]) + (assert (= (wayne) 22))))) diff --git a/tests/native_tests/keywords.hy b/tests/native_tests/keywords.hy new file mode 100644 index 000000000..981e39b3d --- /dev/null +++ b/tests/native_tests/keywords.hy @@ -0,0 +1,99 @@ +(import + pickle + pytest + tests.resources [kwtest]) + + +(defn test-keyword [] + (assert (= :foo :foo)) + (assert (= :foo ':foo)) + (setv x :foo) + (assert (is (type x) (type ':foo))) + (assert (= (get {:foo "bar"} :foo) "bar")) + (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux"))) + + +(defn test-keyword-clash [] + "Keywords shouldn't clash with normal strings." + + (assert (= (get {:foo "bar" ":foo" "quux"} :foo) "bar")) + (assert (= (get {:foo "bar" ":foo" "quux"} ":foo") "quux"))) + + +(defn test-empty-keyword [] + (assert (= : :)) + (assert (isinstance ': hy.models.Keyword)) + (assert (!= : ":")) + (assert (= (. ': name) ""))) + + +(defn test-pickling-keyword [] + ; https://github.com/hylang/hy/issues/1754 + (setv x :test-keyword) + (for [protocol (range 0 (+ pickle.HIGHEST-PROTOCOL 1))] + (assert (= x + (pickle.loads (pickle.dumps x :protocol protocol)))))) + + +(defn test-keyword-get [] + + (assert (= (:foo (dict :foo "test")) "test")) + (setv f :foo) + (assert (= (f (dict :foo "test")) "test")) + + (assert (= (:foo-bar (dict :foo-bar "baz")) "baz")) + (assert (= (:♥ (dict :♥ "heart")) "heart")) + (defclass C [] + (defn __getitem__ [self k] + k)) + (assert (= (:♥ (C)) "hyx_Xblack_heart_suitX")) + + (with [(pytest.raises KeyError)] (:foo (dict :a 1 :b 2))) + (assert (= (:foo (dict :a 1 :b 2) 3) 3)) + (assert (= (:foo (dict :a 1 :b 2 :foo 5) 3) 5)) + + (with [(pytest.raises TypeError)] (:foo "Hello World")) + (with [(pytest.raises TypeError)] (:foo (object))) + + ; The default argument should work regardless of the collection type. + (defclass G [object] + (defn __getitem__ [self k] + (raise KeyError))) + (assert (= (:foo (G) 15) 15))) + + +(defn test-keyword-creation [] + (assert (= (hy.models.Keyword "foo") :foo)) + (assert (= (hy.models.Keyword "foo_bar") :foo_bar)) + (assert (= (hy.models.Keyword "foo-bar") :foo-bar)) + (assert (!= :foo_bar :foo-bar)) + (assert (= (hy.models.Keyword "") :))) + + +(defn test-keywords-in-fn-calls [] + (assert (= (kwtest) {})) + (assert (= (kwtest :key "value") {"key" "value"})) + (assert (= (kwtest :key-with-dashes "value") {"key_with_dashes" "value"})) + (assert (= (kwtest :result (+ 1 1)) {"result" 2})) + (assert (= (kwtest :key (kwtest :key2 "value")) {"key" {"key2" "value"}})) + (assert (= ((get (kwtest :key (fn [x] (* x 2))) "key") 3) 6))) + + +(defn test-kwargs [] + (assert (= (kwtest :one "two") {"one" "two"})) + (setv mydict {"one" "three"}) + (assert (= (kwtest #** mydict) mydict)) + (assert (= (kwtest #** ((fn [] {"one" "two"}))) {"one" "two"}))) + + +(defmacro identify-keywords [#* elts] + `(list + (map + (fn [x] (if (isinstance x hy.models.Keyword) "keyword" "other")) + ~elts))) + +(defn test-keywords-and-macros [] + "Macros should still be able to handle keywords as they best see fit." + (assert + (= (identify-keywords 1 "bloo" :foo) + ["other" "other" "keyword"]))) diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy deleted file mode 100644 index a88ff672a..000000000 --- a/tests/native_tests/language.hy +++ /dev/null @@ -1,1777 +0,0 @@ -(import tests.resources [kwtest function-with-a-dash AsyncWithTest] - os.path [exists isdir isfile] - os - sys :as systest - re - operator [or_] - itertools [repeat count islice] - pickle - typing [get-type-hints List Dict] - asyncio - hy.errors [HyLanguageError HySyntaxError] - pytest) -(import sys) - -(import hy._compat [PY3_8]) - - -(defmacro mac [x expr] - `(~@expr ~x)) - - -(defn test-sys-argv [] - ;; BTW, this also tests inline comments. Which suck to implement. - (assert (isinstance sys.argv list))) - - -(defn test-hex [] - (assert (= 0x80 128))) - - -(defn test-octal [] - (assert (= 0o1232 666))) - - -(defn test-binary [] - (assert (= 0b1011101 93))) - - -(defn test-lists [] - (assert (= [1 2 3 4] (+ [1 2] [3 4])))) - - -(defn test-dicts [] - (assert (= {1 2 3 4} {3 4 1 2})) - (assert (= {1 2 3 4} {1 (+ 1 1) 3 (+ 2 2)}))) - - -(defn test-sets [] - (assert (= #{1 2 3 4} (| #{1 2} #{3 4}))) - (assert (= (type #{1 2 3 4}) set)) - (assert (= #{} (set)))) - - -(defn test-setv-get [] - (setv foo [0 1 2]) - (setv (get foo 0) 12) - (assert (= (get foo 0) 12))) - - -(defn test-setv-pairs [] - (setv a 1 b 2) - (assert (= a 1)) - (assert (= b 2)) - (setv y 0 x 1 y x) - (assert (= y 1)) - (with [(pytest.raises HyLanguageError)] - (hy.eval '(setv a 1 b)))) - - -(defn test-setv-returns-none [] - - (defn an [x] - (assert (is x None))) - - (an (setv)) - (an (setv x 1)) - (assert (= x 1)) - (an (setv x 2)) - (assert (= x 2)) - (an (setv y 2 z 3)) - (assert (= y 2)) - (assert (= z 3)) - (an (setv [y z] [7 8])) - (assert (= y 7)) - (assert (= z 8)) - (an (setv #(y z) [9 10])) - (assert (= y 9)) - (assert (= z 10)) - - (setv p 11) - (setv p (setv q 12)) - (assert (= q 12)) - (an p) - - (an (setv x (defn phooey [] (setv p 1) (+ p 6)))) - (an (setv x (defclass C))) - (an (setv x (for [i (range 3)] i (+ i 1)))) - (an (setv x (assert True))) - - (an (setv x (with [(open "README.md" "r")] 3))) - (assert (= x 3)) - (an (setv x (try (/ 1 2) (except [ZeroDivisionError] "E1")))) - (assert (= x .5)) - (an (setv x (try (/ 1 0) (except [ZeroDivisionError] "E2")))) - (assert (= x "E2")) - - ; https://github.com/hylang/hy/issues/1052 - (an (setv (get {} "x") 42)) - (setv l []) - (defclass Foo [object] - (defn __setattr__ [self attr val] - (.append l [attr val]))) - (setv x (Foo)) - (an (setv x.eggs "ham")) - (assert (not (hasattr x "eggs"))) - (assert (= l [["eggs" "ham"]]))) - - -(defn test-illegal-assignments [] - (for [form '[ - (setv (do 1 2) 1) - (setv 1 1) - (setv {1 2} 1) - (del 1 1) - ; https://github.com/hylang/hy/issues/1780 - (setv None 1) - (setv False 1) - (setv True 1) - (defn None [] (print "hello")) - (defn True [] (print "hello")) - (defn f [True] (print "hello")) - (for [True [1 2 3]] (print "hello")) - (lfor True [1 2 3] True) - (lfor :setv True 1 True) - (with [True x] (print "hello")) - (try 1 (except [True AssertionError] 2)) - (defclass True [])]] - (with [e (pytest.raises HyLanguageError)] - (hy.eval form)) - (assert (in "Can't assign" e.value.msg)))) - - -(defn test-no-str-as-sym [] - "Don't treat strings as symbols in the calling position" - (with [(pytest.raises TypeError)] ("setv" True 3)) ; A special form - (with [(pytest.raises TypeError)] ("abs" -2)) ; A function - (with [(pytest.raises TypeError)] ("when" 1 2))) ; A macro - - -(defn test-while-loop [] - (setv count 5) - (setv fact 1) - (while (> count 0) - (setv fact (* fact count)) - (setv count (- count 1))) - (assert (= count 0)) - (assert (= fact 120)) - - (setv l []) - (defn f [] - (.append l 1) - (len l)) - (while (!= (f) 4)) - (assert (= l [1 1 1 1])) - - (setv l []) - (defn f [] - (.append l 1) - (len l)) - (while (!= (f) 4) (do)) - (assert (= l [1 1 1 1])) - - ; only compile the condition once - ; https://github.com/hylang/hy/issues/1790 - (global while-cond-var) - (setv while-cond-var 10) - (hy.eval - '(do - (defmacro while-cond [] - (global while-cond-var) - (assert (= while-cond-var 10)) - (+= while-cond-var 1) - `(do - (setv x 3) - False)) - (while (while-cond)) - (assert (= x 3))))) - -(defn test-while-loop-else [] - (setv count 5) - (setv fact 1) - (setv myvariable 18) - (while (> count 0) - (setv fact (* fact count)) - (setv count (- count 1)) - (else (setv myvariable 26))) - (assert (= count 0)) - (assert (= fact 120)) - (assert (= myvariable 26)) - - ; multiple statements in a while loop should work - (setv count 5) - (setv fact 1) - (setv myvariable 18) - (setv myothervariable 15) - (while (> count 0) - (setv fact (* fact count)) - (setv count (- count 1)) - (else (setv myvariable 26) - (setv myothervariable 24))) - (assert (= count 0)) - (assert (= fact 120)) - (assert (= myvariable 26)) - (assert (= myothervariable 24)) - - ; else clause shouldn't get run after a break - (while True - (break) - (else (setv myvariable 53))) - (assert (= myvariable 26)) - - ; don't be fooled by constructs that look like else clauses - (setv x 2) - (setv a []) - (setv else True) - (while x - (.append a x) - (-= x 1) - [else (.append a "e")]) - (assert (= a [2 "e" 1 "e"])) - - (setv x 2) - (setv a []) - (with [(pytest.raises TypeError)] - (while x - (.append a x) - (-= x 1) - ("else" (.append a "e")))) - (assert (= a [2 "e"]))) - - -(defn test-while-multistatement-condition [] - - ; The condition should be executed every iteration, before the body. - ; `else` should be executed last. - (setv s "") - (setv x 2) - (while (do (+= s "a") x) - (+= s "b") - (-= x 1) - (else - (+= s "z"))) - (assert (= s "ababaz")) - - ; `else` should still be skipped after `break`. - (setv s "") - (setv x 2) - (while (do (+= s "a") x) - (+= s "b") - (-= x 1) - (when (= x 0) - (break)) - (else - (+= s "z"))) - (assert (= s "abab")) - - ; `continue` should jump to the condition. - (setv s "") - (setv x 2) - (setv continued? False) - (while (do (+= s "a") x) - (+= s "b") - (when (and (= x 1) (not continued?)) - (+= s "c") - (setv continued? True) - (continue)) - (-= x 1) - (else - (+= s "z"))) - (assert (= s "ababcabaz")) - - ; `break` in a condition applies to the `while`, not an outer loop. - (setv s "") - (for [x "123"] - (+= s x) - (setv y 0) - (while (do (when (and (= x "2") (= y 1)) (break)) (< y 3)) - (+= s "y") - (+= y 1))) - (assert (= s "1yyy2y3yyy")) - - ; The condition is still tested appropriately if its last variable - ; is set to a false value in the loop body. - (setv out []) - (setv x 0) - (setv a [1 1]) - (while (do (.append out 2) (setv x (and a (.pop a))) x) - (setv x 0) - (.append out x)) - (assert (= out [2 0 2 0 2])) - (assert (is x a))) - - -(defn test-branching [] - (if True - (assert (= 1 1)) - (assert (= 2 1)))) - - -(defn test-branching-with-do [] - (if False - (assert (= 2 1)) - (do - (assert (= 1 1)) - (assert (= 1 1)) - (assert (= 1 1))))) - -(defn test-branching-expr-count-with-do [] - "Ensure we execute the right number of expressions in a branch." - (setv counter 0) - (if False - (assert (= 2 1)) - (do - (setv counter (+ counter 1)) - (setv counter (+ counter 1)) - (setv counter (+ counter 1)))) - (assert (= counter 3))) - - -(defn test-cond [] - (cond - (= 1 2) (assert (is True False)) - (is None None) (do (setv x True) (assert x))) - (assert (is (cond) None)) - - (assert (= (cond - False 1 - [] 2 - True 8) 8)) - - (setv x 0) - (assert (is (cond False 1 [] 2 x 3) None)) - - (with [e (pytest.raises hy.errors.HyMacroExpansionError)] - (hy.eval '(cond 1))) - (assert (in "needs an even number of arguments" e.value.msg)) - - ; Make sure each test is only evaluated once, and `cond` - ; short-circuits. - (setv x 1) - (assert (= "first" (cond - (do (*= x 2) True) (do (*= x 3) "first") - (do (*= x 5) True) (do (*= x 7) "second")))) - (assert (= x 6))) - - -(defn test-if [] - (assert (= 1 (if 0 -1 1)))) - -(defn test-index [] - (assert (= (get {"one" "two"} "one") "two")) - (assert (= (get [1 2 3 4 5] 1) 2)) - (assert (= (get {"first" {"second" {"third" "level"}}} - "first" "second" "third") - "level")) - (assert (= (get ((fn [] {"first" {"second" {"third" "level"}}})) - "first" "second" "third") - "level")) - (assert (= (get {"first" {"second" {"third" "level"}}} - ((fn [] "first")) "second" "third") - "level"))) - - -(defn test-fn [] - (setv square (fn [x] (* x x))) - (assert (= 4 (square 2))) - (setv lambda_list (fn [test #* args] #(test args))) - (assert (= #(1 #(2 3)) (lambda_list 1 2 3)))) - - -(defn test-imported-bits [] - (assert (is (exists ".") True)) - (assert (is (isdir ".") True)) - (assert (is (isfile ".") False))) - - -(defn test-star-unpacking [] - ; Python 3-only forms of unpacking are in py3_only_tests.hy - (setv l [1 2 3]) - (setv d {"a" "x" "b" "y"}) - (defn fun [[x1 None] [x2 None] [x3 None] [x4 None] [a None] [b None] [c None]] - [x1 x2 x3 x4 a b c]) - (assert (= (fun 5 #* l) [5 1 2 3 None None None])) - (assert (= (+ #* l) 6)) - (assert (= (fun 5 #** d) [5 None None None "x" "y" None])) - (assert (= (fun 5 #* l #** d) [5 1 2 3 "x" "y" None]))) - - - -(defn test-kwargs [] - (assert (= (kwtest :one "two") {"one" "two"})) - (setv mydict {"one" "three"}) - (assert (= (kwtest #** mydict) mydict)) - (assert (= (kwtest #** ((fn [] {"one" "two"}))) {"one" "two"}))) - - - -(defn test-dotted [] - (assert (= (.join " " ["one" "two"]) "one two")) - - (defclass X [object] []) - (defclass M [object] - (defn meth [self #* args #** kwargs] - (.join " " (+ #("meth") args - (tuple (map (fn [k] (get kwargs k)) (sorted (.keys kwargs)))))))) - - (setv x (X)) - (setv m (M)) - - (assert (= (.meth m) "meth")) - (assert (= (.meth m "foo" "bar") "meth foo bar")) - (assert (= (.meth :b "1" :a "2" m "foo" "bar") "meth foo bar 2 1")) - (assert (= (.meth m #* ["foo" "bar"]) "meth foo bar")) - - (setv x.p m) - (assert (= (.p.meth x) "meth")) - (assert (= (.p.meth x "foo" "bar") "meth foo bar")) - (assert (= (.p.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) - (assert (= (.p.meth x #* ["foo" "bar"]) "meth foo bar")) - - (setv x.a (X)) - (setv x.a.b m) - (assert (= (.a.b.meth x) "meth")) - (assert (= (.a.b.meth x "foo" "bar") "meth foo bar")) - (assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1")) - (assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar")) - - (assert (= (.__str__ :foo) ":foo"))) - - -(defn test-do [] - (do)) - - -(defn test-try [] - - (try (do) (except [])) - - (try (do) (except [IOError]) (except [])) - - ; test that multiple statements in a try get evaluated - (setv value 0) - (try (+= value 1) (+= value 2) (except [IOError]) (except [])) - (assert (= value 3)) - - ; test that multiple expressions in a try get evaluated - ; https://github.com/hylang/hy/issues/1584 - (setv l []) - (defn f [] (.append l 1)) - (try (f) (f) (f) (except [IOError])) - (assert (= l [1 1 1])) - (setv l []) - (try (f) (f) (f) (except [IOError]) (else (f))) - (assert (= l [1 1 1 1])) - - ;; Test correct (raise) - (setv passed False) - (try - (try - (do) - (raise IndexError) - (except [IndexError] (raise))) - (except [IndexError] - (setv passed True))) - (assert passed) - - ;; Test incorrect (raise) - (setv passed False) - (try - (raise) - (except [RuntimeError] - (setv passed True))) - (assert passed) - - ;; Test (finally) - (setv passed False) - (try - (do) - (finally (setv passed True))) - (assert passed) - - ;; Test (finally) + (raise) - (setv passed False) - (try - (raise Exception) - (except []) - (finally (setv passed True))) - (assert passed) - - - ;; Test (finally) + (raise) + (else) - (setv passed False - not-elsed True) - (try - (raise Exception) - (except []) - (else (setv not-elsed False)) - (finally (setv passed True))) - (assert passed) - (assert not-elsed) - - (try - (raise (KeyError)) - (except [[IOError]] (assert False)) - (except [e [KeyError]] (assert e))) - - (try - (raise (KeyError)) - (except [[IOError]] (assert False)) - (except [e [KeyError]] (assert e))) - - (try - (get [1] 3) - (except [IndexError] (assert True)) - (except [IndexError] (do))) - - (try - (print foobar42ofthebaz) - (except [IndexError] (assert False)) - (except [NameError] (do))) - - (try - (get [1] 3) - (except [e IndexError] (assert (isinstance e IndexError)))) - - (try - (get [1] 3) - (except [e [IndexError NameError]] (assert (isinstance e IndexError)))) - - (try - (print foobar42ofthebaz) - (except [e [IndexError NameError]] (assert (isinstance e NameError)))) - - (try - (print foobar42) - (except [[IndexError NameError]] (do))) - - (try - (get [1] 3) - (except [[IndexError NameError]] (do))) - - (try - (print foobar42ofthebaz) - (except [])) - - (try - (print foobar42ofthebaz) - (except [] (do))) - - (try - (print foobar42ofthebaz) - (except [] - (setv foobar42ofthebaz 42) - (assert (= foobar42ofthebaz 42)))) - - (setv passed False) - (try - (try (do) (except []) (else (bla))) - (except [NameError] (setv passed True))) - (assert passed) - - (setv x 0) - (try - (raise IOError) - (except [IOError] - (setv x 45)) - (else (setv x 44))) - (assert (= x 45)) - - (setv x 0) - (try - (raise KeyError) - (except [] - (setv x 45)) - (else (setv x 44))) - (assert (= x 45)) - - (setv x 0) - (try - (try - (raise KeyError) - (except [IOError] - (setv x 45)) - (else (setv x 44))) - (except [])) - (assert (= x 0)) - - ; test that [except ...] and ("except" ...) aren't treated like (except ...), - ; and that the code there is evaluated normally - (setv x 0) - (try - (+= x 1) - ("except" [IOError] (+= x 1)) - (except [])) - - (assert (= x 2)) - - (setv x 0) - (try - (+= x 1) - [except [IOError] (+= x 1)] - (except [])) - - (assert (= x 2))) - - -(defn test-pass [] - (if True (do) (do)) - (assert (= 1 1))) - - -(defn test-yield [] - (defn gen [] (for [x [1 2 3 4]] (yield x))) - (setv ret 0) - (for [y (gen)] (setv ret (+ ret y))) - (assert (= ret 10))) - -(defn test-yield-with-return [] - (defn gen [] (yield 3) "goodbye") - (setv gg (gen)) - (assert (= 3 (next gg))) - (with [e (pytest.raises StopIteration)] - (next gg)) - (assert (= e.value.value "goodbye"))) - - -(defn test-yield-in-try [] - (defn gen [] - (setv x 1) - (try (yield x) - (finally (print x)))) - (setv output (list (gen))) - (assert (= [1] output))) - - -(defn test-ellipsis [] - (global Ellipsis) - (assert (is ... Ellipsis)) - (setv e Ellipsis) - (setv Ellipsis 14) - (assert (= Ellipsis 14)) - (assert (!= ... 14)) - (assert (is ... e))) - - -(defn test-cut [] - (assert (= (cut [1 2 3 4 5] 3) [1 2 3])) - (assert (= (cut [1 2 3 4 5] 1 None) [2 3 4 5])) - (assert (= (cut [1 2 3 4 5] 1 3) [2 3])) - (assert (= (cut [1 2 3 4 5]) [1 2 3 4 5]))) - - -(defn test-importas [] - (assert (!= (len systest.path) 0))) - - -(defn test-context [] - (with [fd (open "README.md" "r")] (assert fd)) - (with [(open "README.md" "r")] (do))) - - -(defn test-with-return [] - (defn read-file [filename] - (with [fd (open filename "r")] (.read fd))) - (assert (!= 0 (len (read-file "README.md"))))) - - -(defn test-for-do [] - (do (do (do (do (do (do (do (do (do (setv #(x y) #(0 0))))))))))) - (for [- [1 2]] - (do - (setv x (+ x 1)) - (setv y (+ y 1)))) - (assert (= y x 2))) - - -(defn test-for-else [] - (setv x 0) - (for [a [1 2]] - (setv x (+ x a)) - (else (setv x (+ x 50)))) - (assert (= x 53)) - - (setv x 0) - (for [a [1 2]] - (setv x (+ x a)) - (else)) - (assert (= x 3))) - - -(defn test-defn-evaluation-order [] - (setv acc []) - (defn my-fun [] - (.append acc "Foo") - (.append acc "Bar") - (.append acc "Baz")) - (my-fun) - (assert (= acc ["Foo" "Bar" "Baz"]))) - - -(defn test-defn-return [] - (defn my-fun [x] - (+ x 1)) - (assert (= 43 (my-fun 42)))) - - -(defn test-defn-lambdakey [] - "Test defn with a `&symbol` function name." - (defn &hy [] 1) - (assert (= (&hy) 1))) - - -(defn test-defn-evaluation-order-with-do [] - (setv acc []) - (defn my-fun [] - (do - (.append acc "Foo") - (.append acc "Bar") - (.append acc "Baz"))) - (my-fun) - (assert (= acc ["Foo" "Bar" "Baz"]))) - - -(defn test-defn-do-return [] - (defn my-fun [x] - (do - (+ x 42) ; noop - (+ x 1))) - (assert (= 43 (my-fun 42)))) - - -(defn test-defn-dunder-name [] - "`defn` should preserve `__name__`." - - (defn phooey [x] - (+ x 1)) - (assert (= phooey.__name__ "phooey")) - - (defn mooey [x] - (+= x 1) - x) - (assert (= mooey.__name__ "mooey"))) - - -(defn test-defn-annotations [] - - (defn #^int f [#^(get List int) p1 p2 #^str p3 #^str [o1 None] #^int [o2 0] - #^str #* rest #^str k1 #^int [k2 0] #^bool #** kwargs]) - - (assert (is (. f __annotations__ ["return"]) int)) - (for [[k v] (.items (dict - :p1 (get List int) :p3 str :o1 str :o2 int - :k1 str :k2 int :kwargs bool))] - (assert (= (. f __annotations__ [k]) v)))) - - -(defn test-return [] - - ; `return` in main line - (defn f [x] - (return (+ x "a")) - (+ x "b")) - (assert (= (f "q") "qa")) - - ; Nullary `return` - (defn f [x] - (return) - 5) - (assert (is (f "q") None)) - - ; `return` in `when` - (defn f [x] - (when (< x 3) - (return [x 1])) - [x 2]) - (assert (= (f 2) [2 1])) - (assert (= (f 4) [4 2])) - - ; `return` in a loop - (setv accum []) - (defn f [x] - (while True - (when (= x 0) - (return)) - (.append accum x) - (-= x 1)) - (.append accum "this should never be appended") - 1) - (assert (is (f 5) None)) - (assert (= accum [5 4 3 2 1])) - - ; `return` of a `do` - (setv accum []) - (defn f [] - (return (do - (.append accum 1) - 3)) - 4) - (assert (= (f) 3)) - (assert (= accum [1])) - - ; `return` of an `if` that will need to be compiled to a statement - (setv accum []) - (defn f [x] - (return (if (= x 1) - (do - (.append accum 1) - "a") - (do - (.append accum 2) - "b"))) - "c") - (assert (= (f 2) "b")) - (assert (= accum [2]))) - - -(defn test-immediately-call-lambda [] - (assert (= 2 ((fn [] (+ 1 1)))))) - - -(defn test-fn-return [] - (setv fn-test ((fn [] (fn [] (+ 1 1))))) - (assert (= (fn-test) 2)) - (setv fn-test (fn [])) - (assert (= (fn-test) None))) - - -(defn test-returnable-ifs [] - (assert (= True (if True True True)))) - - -(defn test-macro-call-in-called-lambda [] - (assert (= ((fn [] (mac 2 (- 10 1)))) 7))) - - -(defn test-and [] - - (setv and123 (and 1 2 3) - and-false (and 1 False 3) - and-true (and) - and-single (and 1)) - (assert (= and123 3)) - (assert (= and-false False)) - (assert (= and-true True)) - (assert (= and-single 1)) - ; short circuiting - (setv a 1) - (and 0 (setv a 2)) - (assert (= a 1))) - -(defn test-and-#1151-do [] - (setv a (and 0 (do 2 3))) - (assert (= a 0)) - (setv a (and 1 (do 2 3))) - (assert (= a 3))) - -(defn test-and-#1151-for [] - (setv l []) - (setv x (and 0 (for [n [1 2]] (.append l n)))) - (assert (= x 0)) - (assert (= l [])) - (setv x (and 15 (for [n [1 2]] (.append l n)))) - (assert (= l [1 2]))) - -(defn test-and-#1151-del [] - (setv l ["a" "b"]) - (setv x (and 0 (del (get l 1)))) - (assert (= x 0)) - (assert (= l ["a" "b"])) - (setv x (and 15 (del (get l 1)))) - (assert (= l ["a"]))) - - -(defn test-or [] - (setv or-all-true (or 1 2 3 True "string") - or-some-true (or False "hello") - or-none-true (or False False) - or-false (or) - or-single (or 1)) - (assert (= or-all-true 1)) - (assert (= or-some-true "hello")) - (assert (= or-none-true False)) - (assert (= or-false None)) - (assert (= or-single 1)) - ; short circuiting - (setv a 1) - (or 1 (setv a 2)) - (assert (= a 1))) - -(defn test-or-#1151-do [] - (setv a (or 1 (do 2 3))) - (assert (= a 1)) - (setv a (or 0 (do 2 3))) - (assert (= a 3))) - -(defn test-or-#1151-for [] - (setv l []) - (setv x (or 15 (for [n [1 2]] (.append l n)))) - (assert (= x 15)) - (assert (= l [])) - (setv x (or 0 (for [n [1 2]] (.append l n)))) - (assert (= l [1 2]))) - -(defn test-or-#1151-del [] - (setv l ["a" "b"]) - (setv x (or 15 (del (get l 1)))) - (assert (= x 15)) - (assert (= l ["a" "b"])) - (setv x (or 0 (del (get l 1)))) - (assert (= l ["a"]))) - -(defn test-if-return-branching [] - ; thanks, kirbyfan64 - (defn f [] - (if True (setv x 1) 2) - 1) - - (assert (= 1 (f)))) - - -(defn test-keyword [] - - (assert (= :foo :foo)) - (assert (= :foo ':foo)) - (setv x :foo) - (assert (is (type x) (type ':foo))) - (assert (= (get {:foo "bar"} :foo) "bar")) - (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux"))) - - -(defn test-keyword-clash [] - "Keywords shouldn't clash with normal strings." - - (assert (= (get {:foo "bar" ":foo" "quux"} :foo) "bar")) - (assert (= (get {:foo "bar" ":foo" "quux"} ":foo") "quux"))) - - -(defn test-empty-keyword [] - (assert (= : :)) - (assert (isinstance ': hy.models.Keyword)) - (assert (!= : ":")) - (assert (= (. ': name) ""))) - -(defn test-pickling-keyword [] - ; https://github.com/hylang/hy/issues/1754 - (setv x :test-keyword) - (for [protocol (range 0 (+ pickle.HIGHEST-PROTOCOL 1))] - (assert (= x - (pickle.loads (pickle.dumps x :protocol protocol)))))) - -(defn test-nested-if [] - (for [x (range 10)] - (if (in "foo" "foobar") - (do - (if True True True)) - (do - (if False False False))))) - - -(defn test-eval [] - (assert (= 2 (hy.eval (quote (+ 1 1))))) - (setv x 2) - (assert (= 4 (hy.eval (quote (+ x 2))))) - (setv test-payload (quote (+ x 2))) - (setv x 4) - (assert (= 6 (hy.eval test-payload))) - (assert (= 9 ((hy.eval (quote (fn [x] (+ 3 3 x)))) 3))) - (assert (= 1 (hy.eval (quote 1)))) - (assert (= "foobar" (hy.eval (quote "foobar")))) - (setv x (quote 42)) - (assert (= 42 (hy.eval x))) - (assert (= 27 (hy.eval (+ (quote (*)) (* [(quote 3)] 3))))) - (assert (= None (hy.eval (quote (print ""))))) - - ;; https://github.com/hylang/hy/issues/1041 - (assert (is (hy.eval 're) re)) - (assert (is ((fn [] (hy.eval 're))) re))) - - -(defn test-eval-false [] - (assert (is (hy.eval 'False) False)) - (assert (is (hy.eval 'None) None)) - (assert (= (hy.eval '0) 0)) - (assert (= (hy.eval '"") "")) - (assert (= (hy.eval 'b"") b"")) - (assert (= (hy.eval ':) :)) - (assert (= (hy.eval '[]) [])) - (assert (= (hy.eval '#()) #())) - (assert (= (hy.eval '{}) {})) - (assert (= (hy.eval '#{}) #{}))) - - -(defn test-eval-global-dict [] - (assert (= 'bar (hy.eval (quote foo) {"foo" 'bar}))) - (assert (= 1 (do (setv d {}) (hy.eval '(setv x 1) d) (hy.eval (quote x) d)))) - (setv d1 {} d2 {}) - (hy.eval '(setv x 1) d1) - (with [e (pytest.raises NameError)] - (hy.eval (quote x) d2))) - -(defn test-eval-failure [] - ; yo dawg - (with [(pytest.raises TypeError)] (hy.eval '(hy.eval))) - (defclass C) - (with [(pytest.raises TypeError)] (hy.eval (C))) - (with [(pytest.raises TypeError)] (hy.eval 'False [])) - (with [(pytest.raises TypeError)] (hy.eval 'False {} 1))) - -(defn test-eval-quasiquote [] - ; https://github.com/hylang/hy/issues/1174 - - (for [x [ - None False True - 5 5.1 - 5j 5.1j 2+1j 1.2+3.4j - "" b"" - "apple bloom" b"apple bloom" "⚘" b"\x00" - [] #{} {} - [1 2 3] #{1 2 3} {"a" 1 "b" 2}]] - (assert (= (hy.eval `(get [~x] 0)) x)) - (assert (= (hy.eval x) x))) - - (setv kw :mykeyword) - (assert (= (get (hy.eval `[~kw]) 0) kw)) - (assert (= (hy.eval kw) kw)) - - (assert (= (hy.eval #()) #())) - (assert (= (hy.eval #(1 2 3)) #(1 2 3))) - - (assert (= (hy.eval `(+ "a" ~(+ "b" "c"))) "abc")) - - (setv l ["a" "b"]) - (setv n 1) - (assert (= (hy.eval `(get ~l ~n) "b"))) - - (setv d {"a" 1 "b" 2}) - (setv k "b") - (assert (= (hy.eval `(get ~d ~k)) 2))) - - -(defn test-quote-bracket-string-delim [] - (assert (= (. '#[my delim[hello world]my delim] brackets) "my delim")) - (assert (= (. '#[[squid]] brackets) "")) - (assert (is (. '"squid" brackets) None))) - - -(defn test-format-strings [] - (assert (= f"hello world" "hello world")) - (assert (= f"hello {(+ 1 1)} world" "hello 2 world")) - (assert (= f"a{ (.upper (+ "g" "k")) }z" "aGKz")) - (assert (= f"a{1}{2}b" "a12b")) - - ; Referring to a variable - (setv p "xyzzy") - (assert (= f"h{p}j" "hxyzzyj")) - - ; Including a statement and setting a variable - (assert (= f"a{(do (setv floop 4) (* floop 2))}z" "a8z")) - (assert (= floop 4)) - - ; Comments - (assert (= f"a{(+ 1 - 2 ; This is a comment. - 3)}z" "a6z")) - - ; Newlines in replacement fields - (assert (= f"ey {"bee -cee"} dee" "ey bee\ncee dee")) - - ; Conversion characters and format specifiers - (setv p:9 "other") - (setv !r "bar") - (assert (= f"a{p !r}" "a'xyzzy'")) - (assert (= f"a{p :9}" "axyzzy ")) - (assert (= f"a{p:9}" "aother")) - (assert (= f"a{p !r :9}" "a'xyzzy' ")) - (assert (= f"a{p !r:9}" "a'xyzzy' ")) - (assert (= f"a{p:9 :9}" "aother ")) - (assert (= f"a{!r}" "abar")) - (assert (= f"a{!r !r}" "a'bar'")) - - ; Fun with `r` - (assert (= f"hello {r"\n"}" r"hello \n")) - (assert (= f"hello {"\n"}" "hello \n")) - - ; Braces escaped via doubling - (assert (= f"ab{{cde" "ab{cde")) - (assert (= f"ab{{cde}}}}fg{{{{{{" "ab{cde}}fg{{{")) - (assert (= f"ab{{{(+ 1 1)}}}" "ab{2}")) - - ; Nested replacement fields - (assert (= f"{2 :{(+ 2 2)}}" " 2")) - (setv value 12.34 width 10 precision 4) - (assert (= f"result: {value :{width}.{precision}}" "result: 12.34")) - - ; Nested replacement fields with ! and : - (defclass C [object] - (defn __format__ [self format-spec] - (+ "C[" format-spec "]"))) - (assert (= f"{(C) : {(str (+ 1 1)) !r :x<5}}" "C[ '2'xx]")) - - ; \N sequences - ; https://github.com/hylang/hy/issues/2321 - (setv ampersand "wich") - (assert (= f"sand{ampersand} \N{ampersand} chips" "sandwich & chips")) - - ; Format bracket strings - (assert (= #[f[a{p !r :9}]f] "a'xyzzy' ")) - (assert (= #[f-string[result: {value :{width}.{precision}}]f-string] - "result: 12.34")) - - ; Quoting shouldn't evaluate the f-string immediately - ; https://github.com/hylang/hy/issues/1844 - (setv quoted 'f"hello {world}") - (assert (isinstance quoted hy.models.FString)) - (with [(pytest.raises NameError)] - (hy.eval quoted)) - (setv world "goodbye") - (assert (= (hy.eval quoted) "hello goodbye")) - - ;; '=' debugging syntax. - (setv foo "bar") - (assert (= f"{foo =}" "foo ='bar'")) - - ;; Whitespace is preserved. - (assert (= f"xyz{ foo = }" "xyz foo = 'bar'")) - - ;; Explicit conversion is applied. - (assert (= f"{ foo = !s}" " foo = bar")) - - ;; Format spec supercedes implicit conversion. - (setv pi 3.141593 fill "_") - (assert (= f"{pi = :{fill}^8.2f}" "pi = __3.14__")) - - ;; Format spec doesn't clobber the explicit conversion. - (with [(pytest.raises - ValueError - :match r"Unknown format code '?f'? for object of type 'str'")] - f"{pi =!s:.3f}") - - ;; Nested "=" is parsed, but fails at runtime, like Python. - (setv width 7) - (with [(pytest.raises - ValueError - :match r"I|invalid format spec(?:ifier)?")] - f"{pi =:{fill =}^{width =}.2f}")) - - -(defn test-format-string-repr-roundtrip [] - (for [orig [ - 'f"hello {(+ 1 1)} world" - 'f"a{p !r:9}" - 'f"{ foo = !s}"]] - (setv new (eval (repr orig))) - (assert (= (len new) (len orig))) - (for [[n o] (zip new orig)] - (when (hasattr o "conversion") - (assert (= n.conversion o.conversion))) - (assert (= n o))))) - - -(defn test-repr-with-brackets [] - (assert (= (repr '"foo") "hy.models.String('foo')")) - (assert (= (repr '#[[foo]]) "hy.models.String('foo', brackets='')")) - (assert (= (repr '#[xx[foo]xx]) "hy.models.String('foo', brackets='xx')")) - (assert (= (repr '#[xx[]xx]) "hy.models.String('', brackets='xx')")) - - (for [g [repr str]] - (defn f [x] (re.sub r"\n\s+" "" (g x) :count 1)) - (assert (= (f 'f"foo") - "hy.models.FString([hy.models.String('foo')])")) - (assert (= (f '#[f[foo]f]) - "hy.models.FString([hy.models.String('foo')], brackets='f')")) - (assert (= (f '#[f-x[foo]f-x]) - "hy.models.FString([hy.models.String('foo')], brackets='f-x')")) - (assert (= (f '#[f-x[]f-x]) - "hy.models.FString(brackets='f-x')")))) - - -(defn test-import-syntax [] - ;; Simple import - (import sys os) - - ;; from os.path import basename - (import os.path [basename]) - (assert (= (basename "/some/path") "path")) - - ;; import os.path as p - (import os.path :as p) - (assert (= p.basename basename)) - - ;; from os.path import basename as bn - (import os.path [basename :as bn]) - (assert (= bn basename)) - - ;; Multiple stuff to import - (import sys - os.path [dirname] - os.path :as op - os.path [dirname :as dn]) - (assert (= (dirname "/some/path") "/some")) - (assert (= op.dirname dirname)) - (assert (= dn dirname))) - - -(defn test-lambda-keyword-lists [] - (defn foo [x #* xs #** kw] [x xs kw]) - (assert (= (foo 10 20 30) [10 #(20 30) {}]))) - - -(defn test-optional-arguments [] - (defn foo [a b [c None] [d 42]] [a b c d]) - (assert (= (foo 1 2) [1 2 None 42])) - (assert (= (foo 1 2 3) [1 2 3 42])) - (assert (= (foo 1 2 3 4) [1 2 3 4]))) - - -(defn test-undefined-name [] - (with [(pytest.raises NameError)] - xxx)) - - -(defn test-if-in-if [] - (assert (= 42 - (if (if 1 True False) - 42 - 43))) - (assert (= 43 - (if (if 0 True False) - 42 - 43)))) - - -(defn test-try-except-return [] - "Ensure we can return from an `except` form." - (assert (= ((fn [] (try xxx (except [NameError] (+ 1 1))))) 2)) - (setv foo (try xxx (except [NameError] (+ 1 1)))) - (assert (= foo 2)) - (setv foo (try (+ 2 2) (except [NameError] (+ 1 1)))) - (assert (= foo 4))) - - -(defn test-try-else-return [] - "Ensure we can return from the `else` clause of a `try`." - ; https://github.com/hylang/hy/issues/798 - - (assert (= "ef" ((fn [] - (try (+ "a" "b") - (except [NameError] (+ "c" "d")) - (else (+ "e" "f"))))))) - - (setv foo - (try (+ "A" "B") - (except [NameError] (+ "C" "D")) - (else (+ "E" "F")))) - (assert (= foo "EF")) - - ; Check that the lvalue isn't assigned in the main `try` body - ; there's an `else`. - (setv x 1) - (setv y 0) - (setv x - (try (+ "G" "H") - (except [NameError] (+ "I" "J")) - (else - (setv y 1) - (assert (= x 1)) - (+ "K" "L")))) - (assert (= x "KL")) - (assert (= y 1))) - - -(defn test-require [] - (with [(pytest.raises NameError)] - (qplah 1 2 3 4)) - (with [(pytest.raises NameError)] - (parald 1 2 3 4)) - (with [(pytest.raises NameError)] - (✈ 1 2 3 4)) - (with [(pytest.raises NameError)] - (hyx_XairplaneX 1 2 3 4)) - - (require tests.resources.tlib [qplah]) - (assert (= (qplah 1 2 3) [8 1 2 3])) - (with [(pytest.raises NameError)] - (parald 1 2 3 4)) - - (require tests.resources.tlib) - (assert (= (tests.resources.tlib.parald 1 2 3) [9 1 2 3])) - (assert (= (tests.resources.tlib.✈ "silly") "plane silly")) - (assert (= (tests.resources.tlib.hyx_XairplaneX "foolish") "plane foolish")) - (with [(pytest.raises NameError)] - (parald 1 2 3 4)) - - (require tests.resources.tlib :as T) - (assert (= (T.parald 1 2 3) [9 1 2 3])) - (assert (= (T.✈ "silly") "plane silly")) - (assert (= (T.hyx_XairplaneX "foolish") "plane foolish")) - (with [(pytest.raises NameError)] - (parald 1 2 3 4)) - - (require tests.resources.tlib [parald :as p]) - (assert (= (p 1 2 3) [9 1 2 3])) - (with [(pytest.raises NameError)] - (parald 1 2 3 4)) - - (require tests.resources.tlib *) - (assert (= (parald 1 2 3) [9 1 2 3])) - (assert (= (✈ "silly") "plane silly")) - (assert (= (hyx_XairplaneX "foolish") "plane foolish")) - - (require tests.resources [tlib macros :as m exports-none]) - (assert (in "tlib.qplah" __macros__)) - (assert (in (hy.mangle "m.test-macro") __macros__)) - (assert (in (hy.mangle "exports-none.cinco") __macros__)) - (require os [path]) - (with [(pytest.raises hy.errors.HyRequireError)] - (hy.eval '(require tests.resources [does-not-exist]))) - - (require tests.resources.exports *) - (assert (= (casey 1 2 3) [11 1 2 3])) - (assert (= (☘ 1 2 3) [13 1 2 3])) - (with [(pytest.raises NameError)] - (brother 1 2 3 4))) - - -(defn test-require-native [] - (with [(pytest.raises NameError)] - (test-macro-2)) - (import tests.resources.macros) - (with [(pytest.raises NameError)] - (test-macro-2)) - (require tests.resources.macros [test-macro-2]) - (test-macro-2) - (assert (= qup 2))) - - -(defn test-relative-require [] - (require ..resources.macros [test-macro]) - (assert (in "test_macro" __macros__)) - - (require .language-beside [xyzzy]) - (assert (in "xyzzy" __macros__)) - - (require . [language-beside :as lb]) - (assert (in "lb.xyzzy" __macros__))) - - -(defn test-export-objects [] - ; We use `hy.eval` here because of a Python limitation that - ; importing `*` is only allowed at the module level. - (hy.eval '(do - (import tests.resources.exports *) - (assert (= (jan) 21)) - (assert (= (♥) 23)) - (with [(pytest.raises NameError)] - (wayne)) - (import tests.resources.exports [wayne]) - (assert (= (wayne) 22))))) - - -(defn test-encoding-nightmares [] - (assert (= (len "ℵℵℵ♥♥♥\t♥♥\r\n") 11))) - - -(defn test-keyword-get [] - - (assert (= (:foo (dict :foo "test")) "test")) - (setv f :foo) - (assert (= (f (dict :foo "test")) "test")) - - (assert (= (:foo-bar (dict :foo-bar "baz")) "baz")) - (assert (= (:♥ (dict :♥ "heart")) "heart")) - (defclass C [] - (defn __getitem__ [self k] - k)) - (assert (= (:♥ (C)) "hyx_Xblack_heart_suitX")) - - (with [(pytest.raises KeyError)] (:foo (dict :a 1 :b 2))) - (assert (= (:foo (dict :a 1 :b 2) 3) 3)) - (assert (= (:foo (dict :a 1 :b 2 :foo 5) 3) 5)) - - (with [(pytest.raises TypeError)] (:foo "Hello World")) - (with [(pytest.raises TypeError)] (:foo (object))) - - ; The default argument should work regardless of the collection type. - (defclass G [object] - (defn __getitem__ [self k] - (raise KeyError))) - (assert (= (:foo (G) 15) 15))) - - -(defn test-break-breaking [] - (defn holy-grail [] (for [x (range 10)] (when (= x 5) (break))) x) - (assert (= (holy-grail) 5))) - - -(defn test-continue-continuation [] - (setv y []) - (for [x (range 10)] - (when (!= x 5) - (continue)) - (.append y x)) - (assert (= y [5]))) - - -(defn test-del [] - (setv foo 42) - (assert (= foo 42)) - (del foo) - (with [(pytest.raises NameError)] - foo) - (setv test (list (range 5))) - (del (get test 4)) - (assert (= test [0 1 2 3])) - (del (get test 2)) - (assert (= test [0 1 3])) - (assert (= (del) None))) - - -(defn test-macroexpand [] - (assert (= (hy.macroexpand '(mac (a b) (x y))) - '(x y (a b)))) - (assert (= (hy.macroexpand '(mac (a b) (mac 5))) - '(a b 5)))) - -(defn test-macroexpand-with-named-import [] - ; https://github.com/hylang/hy/issues/1207 - (defmacro m-with-named-import [] - (import math [pow]) - (pow 2 3)) - (assert (= (hy.macroexpand '(m-with-named-import)) (hy.models.Float (** 2 3))))) - -(defn test-macroexpand-1 [] - (assert (= (hy.macroexpand-1 '(mac (a b) (mac 5))) - '(mac 5 (a b))))) - -(defn test-disassemble [] - (defn nos [x] (re.sub r"\s" "" x)) - (assert (= (nos (hy.disassemble '(do (leaky) (leaky) (macros)))) - (nos (.format - "Module( - body=[Expr(value=Call(func=Name(id='leaky', ctx=Load()), args=[], keywords=[])), - Expr(value=Call(func=Name(id='leaky', ctx=Load()), args=[], keywords=[])), - Expr(value=Call(func=Name(id='macros', ctx=Load()), args=[], keywords=[]))]{})" - (if PY3_8 ",type_ignores=[]" ""))))) - (assert (= (nos (hy.disassemble '(do (leaky) (leaky) (macros)) True)) - "leaky()leaky()macros()")) - (assert (= (re.sub r"[()\n ]" "" (hy.disassemble `(+ ~(+ 1 1) 40) True)) - "2+40"))) - - -(defn test-attribute-access [] - (defclass mycls [object]) - - (setv foo [(mycls) (mycls) (mycls)]) - (assert (is (. foo) foo)) - (assert (is (. foo [0]) (get foo 0))) - (assert (is (. foo [0] __class__) mycls)) - (assert (is (. foo [1] __class__) mycls)) - (assert (is (. foo [(+ 1 1)] __class__) mycls)) - (assert (= (. foo [(+ 1 1)] __class__ __name__ [0]) "m")) - (assert (= (. foo [(+ 1 1)] __class__ __name__ [1]) "y")) - (assert (= (. os (getcwd) (isalpha) __class__ __name__ [0]) "b")) - (assert (= (. "ab hello" (strip "ab ") (upper)) "HELLO")) - (assert (= (. "hElLO\twoRld" (expandtabs :tabsize 4) (lower)) "hello world")) - - (setv bar (mycls)) - (setv (. foo [1]) bar) - (assert (is bar (get foo 1))) - (setv (. foo [1] test) "hello") - (assert (= (getattr (. foo [1]) "test") "hello"))) - -(defn test-only-parse-lambda-list-in-defn [] - (with [(pytest.raises NameError)] - (setv x [#* spam] y 1))) - -(defn test-read-file-object [] - (import io [StringIO]) - - (setv stdin-buffer (StringIO "(+ 2 2)\n(- 2 2)")) - (assert (= (hy.eval (hy.read stdin-buffer)) 4)) - (assert (isinstance (hy.read stdin-buffer) hy.models.Expression)) - - ; Multiline test - (setv stdin-buffer (StringIO "(\n+\n41\n1\n)\n(-\n2\n1\n)")) - (assert (= (hy.eval (hy.read stdin-buffer)) 42)) - (assert (= (hy.eval (hy.read stdin-buffer)) 1)) - - ; EOF test - (setv stdin-buffer (StringIO "(+ 2 2)")) - (hy.read stdin-buffer) - (with [(pytest.raises EOFError)] - (hy.read stdin-buffer))) - -(defn test-read-str [] - (assert (= (hy.read "(print 1)") '(print 1))) - (assert (is (type (hy.read "(print 1)")) (type '(print 1)))) - - ; Watch out for false values: https://github.com/hylang/hy/issues/1243 - (assert (= (hy.read "\"\"") '"")) - (assert (is (type (hy.read "\"\"")) (type '""))) - (assert (= (hy.read "[]") '[])) - (assert (is (type (hy.read "[]")) (type '[]))) - (assert (= (hy.read "0") '0)) - (assert (is (type (hy.read "0")) (type '0)))) - -(defn test-keyword-creation [] - (assert (= (hy.models.Keyword "foo") :foo)) - (assert (= (hy.models.Keyword "foo_bar") :foo_bar)) - (assert (= (hy.models.Keyword "foo-bar") :foo-bar)) - (assert (!= :foo_bar :foo-bar)) - (assert (= (hy.models.Keyword "") :))) - -(defn test-keywords-in-fn-calls [] - (assert (= (kwtest) {})) - (assert (= (kwtest :key "value") {"key" "value"})) - (assert (= (kwtest :key-with-dashes "value") {"key_with_dashes" "value"})) - (assert (= (kwtest :result (+ 1 1)) {"result" 2})) - (assert (= (kwtest :key (kwtest :key2 "value")) {"key" {"key2" "value"}})) - (assert (= ((get (kwtest :key (fn [x] (* x 2))) "key") 3) 6))) - -(defmacro identify-keywords [#* elts] - `(list - (map - (fn [x] (if (isinstance x hy.models.Keyword) "keyword" "other")) - ~elts))) - -(defn test-keywords-and-macros [] - "Macros should still be able to handle keywords as they best see fit." - (assert - (= (identify-keywords 1 "bloo" :foo) - ["other" "other" "keyword"]))) - -(defn test-underscore_variables [] - ; https://github.com/hylang/hy/issues/1340 - (defclass XYZ [] - (setv _42 6)) - (setv x (XYZ)) - (assert (= (. x _42) 6))) - -(defn test-docstrings [] - (defn f [] "docstring" 5) - (assert (= (. f __doc__) "docstring")) - - ; a single string is the return value, not a docstring - ; (https://github.com/hylang/hy/issues/1402) - (defn f3 [] "not a docstring") - (assert (is (. f3 __doc__) None)) - (assert (= (f3) "not a docstring"))) - -(defn test-module-docstring [] - (import tests.resources.module-docstring-example :as m) - (assert (= m.__doc__ "This is the module docstring.")) - (assert (= m.foo 5))) - -(defn test-relative-import [] - (import ..resources [tlib]) - (assert (= tlib.SECRET-MESSAGE "Hello World"))) - - -(defn test-exception-cause [] - (assert (is NameError (type (. - (try - (raise ValueError :from NameError) - (except [e [ValueError]] e)) - __cause__))))) - - -(defn test-kwonly [] - ;; keyword-only with default works - (defn kwonly-foo-default-false [* [foo False]] foo) - (assert (= (kwonly-foo-default-false) False)) - (assert (= (kwonly-foo-default-false :foo True) True)) - ;; keyword-only without default ... - (defn kwonly-foo-no-default [* foo] foo) - (with [e (pytest.raises TypeError)] - (kwonly-foo-no-default)) - (assert (in "missing 1 required keyword-only argument: 'foo'" - (. e value args [0]))) - ;; works - (assert (= (kwonly-foo-no-default :foo "quux") "quux")) - ;; keyword-only with other arg types works - (defn function-of-various-args [a b #* args foo #** kwargs] - #(a b args foo kwargs)) - (assert (= (function-of-various-args 1 2 3 4 :foo 5 :bar 6 :quux 7) - #(1 2 #(3 4) 5 {"bar" 6 "quux" 7})))) - - -(defn test-extended-unpacking-1star-lvalues [] - (setv [x #*y] [1 2 3 4]) - (assert (= x 1)) - (assert (= y [2 3 4])) - (setv [a #*b c] "ghijklmno") - (assert (= a "g")) - (assert (= b (list "hijklmn"))) - (assert (= c "o"))) - - -(defn test-yield-from [] - (defn yield-from-test [] - (for [i (range 3)] - (yield i)) - (yield-from [1 2 3])) - (assert (= (list (yield-from-test)) [0 1 2 1 2 3]))) - - -(defn test-yield-from-exception-handling [] - (defn yield-from-subgenerator-test [] - (yield 1) - (yield 2) - (yield 3) - (/ 1 0)) - (defn yield-from-test [] - (for [i (range 3)] - (yield i)) - (try - (yield-from (yield-from-subgenerator-test)) - (except [e ZeroDivisionError] - (yield 4)))) - (assert (= (list (yield-from-test)) [0 1 2 1 2 3 4]))) - -(defn test-pep-3115 [] - (defclass member-table [dict] - (defn __init__ [self] - (setv self.member-names [])) - - (defn __setitem__ [self key value] - (when (not-in key self) - (.append self.member-names key)) - (dict.__setitem__ self key value))) - - (defclass OrderedClass [type] - (setv __prepare__ (classmethod (fn [metacls name bases] - (member-table)))) - - (defn __new__ [cls name bases classdict] - (setv result (type.__new__ cls name bases (dict classdict))) - (setv result.member-names classdict.member-names) - result)) - - (defclass MyClass [:metaclass OrderedClass] - (defn method1 [self] (pass)) - (defn method2 [self] (pass))) - - (assert (= (. (MyClass) member-names) - ["__module__" "__qualname__" "method1" "method2"]))) - - -(defn test-unpacking-pep448-1star [] - (setv l [1 2 3]) - (setv p [4 5]) - (assert (= ["a" #*l "b" #*p #*l] ["a" 1 2 3 "b" 4 5 1 2 3])) - (assert (= #("a" #*l "b" #*p #*l) #("a" 1 2 3 "b" 4 5 1 2 3))) - (assert (= #{"a" #*l "b" #*p #*l} #{"a" "b" 1 2 3 4 5})) - (defn f [#* args] args) - (assert (= (f "a" #*l "b" #*p #*l) #("a" 1 2 3 "b" 4 5 1 2 3))) - (assert (= (+ #*l #*p) 15)) - (assert (= (and #*l) 3))) - - -(defn test-unpacking-pep448-2star [] - (setv d1 {"a" 1 "b" 2}) - (setv d2 {"c" 3 "d" 4}) - (assert (= {1 "x" #**d1 #**d2 2 "y"} {"a" 1 "b" 2 "c" 3 "d" 4 1 "x" 2 "y"})) - (defn fun [[a None] [b None] [c None] [d None] [e None] [f None]] - [a b c d e f]) - (assert (= (fun #**d1 :e "eee" #**d2) [1 2 3 4 "eee" None]))) - - -(defn test-fn/a [] - (assert (= (asyncio.run ((fn/a [] (await (asyncio.sleep 0)) [1 2 3]))) - [1 2 3]))) - - -(defn test-defn/a [] - (defn/a coro-test [] - (await (asyncio.sleep 0)) - [1 2 3]) - (assert (= (asyncio.run (coro-test)) [1 2 3]))) - - -(defn test-decorated-defn/a [] - (defn decorator [func] (fn/a [] (/ (await (func)) 2))) - - (defn/a [decorator] coro-test [] - (await (asyncio.sleep 0)) - 42) - (assert (= (asyncio.run (coro-test)) 21))) - - -(defn test-single-with/a [] - (asyncio.run - ((fn/a [] - (with/a [t (AsyncWithTest 1)] - (assert (= t 1))))))) - -(defn test-two-with/a [] - (asyncio.run - ((fn/a [] - (with/a [t1 (AsyncWithTest 1) - t2 (AsyncWithTest 2)] - (assert (= t1 1)) - (assert (= t2 2))))))) - -(defn test-thrice-with/a [] - (asyncio.run - ((fn/a [] - (with/a [t1 (AsyncWithTest 1) - t2 (AsyncWithTest 2) - t3 (AsyncWithTest 3)] - (assert (= t1 1)) - (assert (= t2 2)) - (assert (= t3 3))))))) - -(defn test-quince-with/a [] - (asyncio.run - ((fn/a [] - (with/a [t1 (AsyncWithTest 1) - t2 (AsyncWithTest 2) - t3 (AsyncWithTest 3) - _ (AsyncWithTest 4)] - (assert (= t1 1)) - (assert (= t2 2)) - (assert (= t3 3))))))) - -(defn test-for-async [] - (defn/a numbers [] - (for [i [1 2]] - (yield i))) - - (asyncio.run - ((fn/a [] - (setv x 0) - (for [:async a (numbers)] - (setv x (+ x a))) - (assert (= x 3)))))) - -(defn test-for-async-else [] - (defn/a numbers [] - (for [i [1 2]] - (yield i))) - - (asyncio.run - ((fn/a [] - (setv x 0) - (for [:async a (numbers)] - (setv x (+ x a)) - (else (setv x (+ x 50)))) - (assert (= x 53)))))) - -(defn test-variable-annotations [] - (defclass AnnotationContainer [] - (setv #^int x 1 y 2) - (#^bool z)) - - (setv annotations (get-type-hints AnnotationContainer)) - (assert (= (get annotations "x") int)) - (assert (= (get annotations "z") bool))) - -(defn test-pep-487 [] - (defclass QuestBase [] - (defn __init-subclass__ [cls swallow #** kwargs] - (setv cls.swallow swallow))) - - (defclass Quest [QuestBase :swallow "african"]) - (assert (= (. (Quest) swallow) "african"))) diff --git a/tests/native_tests/let.hy b/tests/native_tests/let.hy index 9ff3eadaf..94f0b1122 100644 --- a/tests/native_tests/let.hy +++ b/tests/native_tests/let.hy @@ -290,6 +290,20 @@ (assert (= fox 42)))) +(defn test-top-level-let-nonlocal [] + (hy.eval '(do + (let [my-fuel 50] + (defn propulse-me [distance] + (nonlocal my-fuel) + (-= my-fuel distance)) + (defn check-fuel [] + my-fuel)) + (assert (= (check-fuel) 50)) + (propulse-me 3) + (assert (= (check-fuel) 47))) + :globals {})) + + (defn test-let-nested-nonlocal [] (let [fox 42] (defn bar [] @@ -505,8 +519,8 @@ #(10 20 30))))) -(defmacro eval-isolated [#*body] - `(hy.eval '(do ~@body) :module "" :locals {})) +(defmacro eval-isolated [#* body] + `(hy.eval '(do ~@body) :module (hy.I.types.ModuleType "") :locals {})) (defn test-let-bound-nonlocal [] diff --git a/tests/native_tests/logic_short_circuit.hy b/tests/native_tests/logic_short_circuit.hy new file mode 100644 index 000000000..44dbe3085 --- /dev/null +++ b/tests/native_tests/logic_short_circuit.hy @@ -0,0 +1,63 @@ +;; More basic tests of `and` and `or` can be found in `operators.hy`. + + +(defn test-and [] + (setv a 1) + (and 0 (setv a 2)) + (assert (= a 1))) + + +(defn test-and-#1151-do [] + (setv a (and 0 (do 2 3))) + (assert (= a 0)) + (setv a (and 1 (do 2 3))) + (assert (= a 3))) + + +(defn test-and-#1151-for [] + (setv l []) + (setv x (and 0 (for [n [1 2]] (.append l n)))) + (assert (= x 0)) + (assert (= l [])) + (setv x (and 15 (for [n [1 2]] (.append l n)))) + (assert (= l [1 2]))) + + +(defn test-and-#1151-del [] + (setv l ["a" "b"]) + (setv x (and 0 (del (get l 1)))) + (assert (= x 0)) + (assert (= l ["a" "b"])) + (setv x (and 15 (del (get l 1)))) + (assert (= l ["a"]))) + + +(defn test-or [] + (setv a 1) + (or 1 (setv a 2)) + (assert (= a 1))) + + +(defn test-or-#1151-do [] + (setv a (or 1 (do 2 3))) + (assert (= a 1)) + (setv a (or 0 (do 2 3))) + (assert (= a 3))) + + +(defn test-or-#1151-for [] + (setv l []) + (setv x (or 15 (for [n [1 2]] (.append l n)))) + (assert (= x 15)) + (assert (= l [])) + (setv x (or 0 (for [n [1 2]] (.append l n)))) + (assert (= l [1 2]))) + + +(defn test-or-#1151-del [] + (setv l ["a" "b"]) + (setv x (or 15 (del (get l 1)))) + (assert (= x 15)) + (assert (= l ["a" "b"])) + (setv x (or 0 (del (get l 1)))) + (assert (= l ["a"]))) diff --git a/tests/native_tests/macros.hy b/tests/native_tests/macros.hy new file mode 100644 index 000000000..6490cb707 --- /dev/null +++ b/tests/native_tests/macros.hy @@ -0,0 +1,187 @@ +(import os sys warnings + pytest + hy.errors [HySyntaxError HyTypeError HyMacroExpansionError]) + +(defmacro rev [#* body] + "Execute the `body` statements in reverse" + (quasiquote (do (unquote-splice (list (reversed body)))))) + +(defmacro mac [x expr] + `(~@expr ~x)) + + + +(defn test-macro-call-in-called-lambda [] + (assert (= ((fn [] (mac 2 (- 10 1)))) 7))) + + +(defn test-stararged-native-macro [] + (setv x []) + (rev (.append x 1) (.append x 2) (.append x 3)) + (assert (= x [3 2 1]))) + +(defn test-macros-returning-constants [] + (defmacro an-int [] 42) + (assert (= (an-int) 42)) + + (defmacro a-true [] True) + (assert (= (a-true) True)) + (defmacro a-false [] False) + (assert (= (a-false) False)) + + (defmacro a-float [] 42.) + (assert (= (a-float) 42.)) + + (defmacro a-complex [] 42j) + (assert (= (a-complex) 42j)) + + (defmacro a-string [] "foo") + (assert (= (a-string) "foo")) + + (defmacro a-bytes [] b"foo") + (assert (= (a-bytes) b"foo")) + + (defmacro a-list [] [1 2]) + (assert (= (a-list) [1 2])) + + (defmacro a-tuple [#* b] b) + (assert (= (a-tuple 1 2) #(1 2))) + + (defmacro a-dict [] {1 2}) + (assert (= (a-dict) {1 2})) + + (defmacro a-set [] #{1 2}) + (assert (= (a-set) #{1 2})) + + (defmacro a-none []) + (assert (= (a-none) None))) + + +; A macro calling a previously defined function +(eval-when-compile + (defn foo [x y] + (quasiquote (+ (unquote x) (unquote y))))) + +(defmacro bar [x y] + (foo x y)) + +(defn test-macro-kw [] + "An error is raised when * or #** is used in a macro" + + (with [(pytest.raises HySyntaxError)] + (hy.eval '(defmacro f [* a b]))) + + (with [(pytest.raises HySyntaxError)] + (hy.eval '(defmacro f [#** kw]))) + + (with [(pytest.raises HySyntaxError)] + (hy.eval '(defmacro f [a b #* body c])))) + +(defn test-macro-bad-name [] + (with [e (pytest.raises HySyntaxError)] + (hy.eval '(defmacro :kw []))) + (assert (in "got unexpected token: :kw" e.value.msg)) + + (with [(pytest.raises HySyntaxError)] + (hy.eval '(defmacro foo.bar [])))) + +(defn test-macro-calling-fn [] + (assert (= 3 (bar 1 2)))) + +(defn test-optional-and-unpacking-in-macro [] + ; https://github.com/hylang/hy/issues/1154 + (defn f [#* args] + (+ "f:" (repr args))) + (defmacro mac [[x None]] + `(f #* [~x])) + (assert (= (mac) "f:(None,)"))) + +(defn test-macro-autoboxing-docstring [] + (defmacro m [] + (setv mystring "hello world") + `(fn [] ~mystring (+ 1 2))) + (setv f (m)) + (assert (= (f) 3)) + (assert (= f.__doc__ "hello world"))) + + +; Macro that checks a variable defined at compile or load time +(setv phase "load") +(eval-when-compile + (setv phase "compile")) +(defmacro phase-when-compiling [] phase) +(assert (= phase "load")) +(assert (= (phase-when-compiling) "compile")) + +(setv initialized False) +(eval-and-compile + (setv initialized True)) +(defmacro test-initialized [] initialized) +(assert initialized) +(assert (test-initialized)) + + +(defmacro gensym-example [] + `(setv ~(hy.gensym) 1)) + +(defn test-gensym-in-macros [] + ; Call `gensym-example` twice, getting a distinct gensym each time. + (defclass C [] + (gensym-example) + (gensym-example)) + (assert (= + (len (sfor a (dir C) :if (not (.startswith a "__")) a)) + 2))) + + +(defn test-macro-errors [] + (import traceback + hy.importer [read-many]) + + (setv test-expr (read-many "(defmacro blah [x] `(print ~@z)) (blah y)")) + + (with [excinfo (pytest.raises HyMacroExpansionError)] + (hy.eval test-expr)) + + (setv output (traceback.format_exception_only + excinfo.type excinfo.value)) + (setv output (cut (.splitlines (.strip (get output 0))) 1 None)) + + (setv expected [" File \"\", line 1" + " (defmacro blah [x] `(print ~@z)) (blah y)" + " ^------^" + "expanding macro blah" + " NameError: global name 'z' is not defined"]) + + (assert (= (cut expected 0 -1) (cut output 0 -1))) + (assert (or (= (get expected -1) (get output -1)) + ;; Handle PyPy's peculiarities + (= (.replace (get expected -1) "global " "") (get output -1)))) + + + ;; This should throw a `HyWrapperError` that gets turned into a + ;; `HyMacroExpansionError`. + (with [excinfo (pytest.raises HyMacroExpansionError)] + (hy.eval '(do (defmacro wrap-error-test [] + (fn [])) + (wrap-error-test)))) + (assert (in "HyWrapperError" (str excinfo.value)))) + + +(defn macro-redefinition-warning-tester [local] + (for [should-warn? [True False] head ["defmacro" "require"]] + (with [(if should-warn? + (pytest.warns RuntimeWarning :match "will shadow the core macro") + (warnings.catch-warnings))] + (when (not should-warn?) + ; Elevate any warning to an error. + (warnings.simplefilter "error")) + (hy.eval `( + ~@(if local '[defn f []] '[do]) + ~(if should-warn? None '(pragma :warn-on-core-shadow False)) + ~(if (= head "defmacro") + '(defmacro when [] 1) + '(require tests.resources.tlib [qplah :as when]))))))) + +(defn test-macro-redefinition-warning [] + (macro-redefinition-warning-tester :local False)) diff --git a/tests/native_tests/macros_first_class.hy b/tests/native_tests/macros_first_class.hy new file mode 100644 index 000000000..aaf3aa345 --- /dev/null +++ b/tests/native_tests/macros_first_class.hy @@ -0,0 +1,103 @@ +"Tests of using macros as first-class objects: listing, creating, and +deleting them, and retrieving their docstrings. We also test `get-macro` +(with regular, non-reader macros)." + +(import + builtins) + +;; * Core macros + +(defn test-core [] + (assert (in "when" (.keys builtins._hy_macros))) + (assert (not-in "global1" (.keys builtins._hy_macros))) + (assert (not-in "nonexistent" (.keys builtins._hy_macros))) + + (assert (is + (get-macro when) + (get-macro "when") + (get builtins._hy_macros "when"))) + + (setv s (. (get-macro when) __doc__)) + (assert s) + (assert (is (type s) str))) + +;; * Global macros + +;; ** Creation + +; There are three ways to define a global macro: +; 1. `defmacro` in global scope +(defmacro global1 [] + "global1 docstring" + "from global1") +; 2. `require` in global scope +(require tests.resources.tlib [qplah :as global2]) +; 3. Manually updating `_hy_macros` +(eval-and-compile (setv (get _hy_macros "global3") (fn [] + "from global3"))) +(eval-and-compile (setv (get _hy_macros (hy.mangle "global☘")) (fn [] + "global☘ docstring" + "from global☘"))) + +(defn test-globals [] + (assert (not-in "when" (.keys _hy_macros))) + (assert (not-in "nonexistent" (.keys _hy_macros))) + (assert (all (gfor + k ["global1" "global2" "global3" "global☘"] + (in (hy.mangle k) (.keys _hy_macros))))) + (assert (= (global3) "from global3")) + (assert (= (global☘) "from global☘")) + (assert (= (. (get-macro global1) __doc__) "global1 docstring")) + ; https://github.com/hylang/hy/issues/1946 + (assert (= (. (get-macro global☘) __doc__) "global☘ docstring")) + (assert (= (. (get-macro hyx_globalXshamrockX) __doc__) "global☘ docstring"))) + +;; ** Deletion +; Try creating and then deleting a global macro. + +(defn global4 [] + "from global4 function") +(setv global4-f1 (global4)) ; Calls the function +(defmacro global4 [] + "from global4 macro") +(setv global4-m (global4)) ; Calls the macro +(eval-when-compile (del (get-macro global4))) +(setv global4-f2 (global4)) ; Calls the function again + +(defn test-global-delete [] + (assert (= (global4) global4-f1 global4-f2 "from global4 function")) + (assert (= global4-m "from global4 macro"))) + +;; ** Shadowing a core macro +; Try overriding a core macro, then deleting the override. + +(pragma :warn-on-core-shadow False) +(defmacro / [a b] + f"{(int a)}/{(int b)}") +(setv div1 (/ 1 2)) +(eval-when-compile (del (get-macro /))) +(setv div2 (/ 1 2)) + +(defn test-global-shadowing-builtin [] + (assert (= div1 "1/2")) + (assert (= div2 0.5))) + +;; * Local macros + +(defn test-local-get [] + (defmacro local1 [] "local1 doc" 1) + (defmacro local2 [] "local2 outer" 2) + (require tests.resources.local-req-example :as LRE) + + (assert (= (. (get-macro local1) __doc__) "local1 doc")) + (assert (= (. (get-macro local2) __doc__) "local2 outer")) + (assert (= (. (get-macro LRE.wiz) __doc__) "remote wiz doc")) + + (defn inner [] + (defmacro local2 [] "local2 inner" 2) + (defmacro local3 [] "local3 doc" 2) + (assert (= (. (get-macro local2) __doc__) "local2 inner")) + (assert (= (. (get-macro local3) __doc__) "local3 doc")) + (assert (= (. (get-macro LRE.wiz) __doc__) "remote wiz doc"))) + + (inner)) diff --git a/tests/native_tests/macros_local.hy b/tests/native_tests/macros_local.hy new file mode 100644 index 000000000..f8639fba0 --- /dev/null +++ b/tests/native_tests/macros_local.hy @@ -0,0 +1,123 @@ +"Tests of local `defmacro` and `require`." + +(import + tests.native-tests.macros [macro-redefinition-warning-tester] + pytest) + + +(defn test-nonleaking [] + (defn fun [] + (defmacro helper [] + "helper macro in fun") + (helper)) + (defclass C [] + (defmacro helper [] + "helper macro in class") + (setv attribute (helper))) + (defn helper [] + "helper function") + (assert (= (helper) "helper function")) + (assert (= (fun) "helper macro in fun")) + (assert (= C.attribute "helper macro in class")) + (assert (= + (lfor + x [1 2 3] + :do (defmacro helper [] + "helper macro in lfor") + y [1 2 3] + (if (= x y 2) (helper) (+ (* x 10) y))) + [11 12 13 21 "helper macro in lfor" 23 31 32 33])) + (assert (= (helper) "helper function"))) + + +(defmacro shadowable [] + "global version") + +(defn test-shadowing-global [] + (defn inner [] + (defmacro shadowable [] + "local version") + (shadowable)) + (assert (= (shadowable) "global version")) + (assert (= (inner) "local version")) + (assert (= (shadowable) "global version"))) + + +(defn test-nested-local-shadowing [] + (defn inner1 [] + (defmacro shadowable [] + "local version 1") + (defn inner2 [] + (defmacro shadowable [] + "local version 2") + (shadowable)) + [(inner2) (shadowable)]) + (assert (= (shadowable) "global version")) + (print (inner1)) + (assert (= (inner1) ["local version 2" "local version 1"])) + (assert (= (shadowable) "global version"))) + + +(defmacro one-plus-two [] + '(+ 1 2)) + +(defn test-local-macro-in-expansion-of-nonlocal [] + (defn f [] + (pragma :warn-on-core-shadow False) + (defmacro + [a b] + "Shadow the core macro `+`. #yolo" + `f"zomg! {~a} {~b}") + (one-plus-two)) + (assert (= (f) "zomg! 1 2")) + (assert (= (one-plus-two) 3))) + + +(defmacro local-require-test [arg] `(do + (defmacro wiz [] + "local wiz") + + (defn fun [] + (require tests.resources.local-req-example ~arg) + [(get-wiz) (helper)]) + (defn helper [] + "local helper function") + + (assert (= [(wiz) (helper)] ["local wiz" "local helper function"])) + (assert (= (fun) ["remote wiz" "remote helper macro"])) + (assert (= [(wiz) (helper)] ["local wiz" "local helper function"])))) + +(defn test-require [] + (local-require-test [get-wiz helper])) + +(defn test-require-star [] + (local-require-test *)) + + +(defn test-redefinition-warning [] + (macro-redefinition-warning-tester :local True)) + + +(defn test-get-local-macros [] + "Test the core macro `local-macros`." + + (defn check [ms inner?] + (assert (= (set (.keys ms)) + #{"m1" "m2" "rwiz" #* (if inner? ["m3" "rhelp"] [])})) + (assert (= (. ms ["m1"] __doc__) "m1 doc")) + (assert (= (. ms ["m2"] __doc__) (if inner? "m2, inner" "m2, outer"))) + (assert (= (. ms ["rwiz"] __doc__) "remote wiz doc")) + (when inner? + (assert (= (. ms ["m3"] __doc__) "m3 doc")) + (assert (= (. ms ["rhelp"] __doc__) "remote helper doc")))) + + (defmacro m1 [] "m1 doc" 1) + (defmacro m2 [] "m2, outer" 2) + (require tests.resources.local-req-example [wiz :as rwiz]) + (check (local-macros) False) + (defn inner-f [] + (defmacro m2 [] "m2, inner" 2) + (defmacro m3 [] "m3 doc" 3) + (require tests.resources.local-req-example [helper :as rhelp]) + (check (local-macros) True)) + (inner-f) + (check (local-macros) False)) diff --git a/tests/native_tests/mangling.hy b/tests/native_tests/mangling.hy index db359483c..5f361c8e7 100644 --- a/tests/native_tests/mangling.hy +++ b/tests/native_tests/mangling.hy @@ -28,8 +28,6 @@ (assert (= (hy.mangle "--__") "hyx_XhyphenHminusX___")) (assert (= (hy.mangle "__--") "__hyx_XhyphenHminusX_")) (assert (= (hy.mangle "__--__") "__hyx_XhyphenHminusX___")) - (assert (= (hy.mangle "--?") "hyx_is_XhyphenHminusX_")) - (assert (= (hy.mangle "__--?") "__hyx_is_XhyphenHminusX_")) ;; test unmangling choices (assert (= (hy.unmangle "hyx_XhyphenHminusX") "-")) @@ -49,12 +47,10 @@ (defn test-question-mark [] + ; Once treated specially, but no more. (setv foo? "nachos") (assert (= foo? "nachos")) - (assert (= is_foo "nachos")) - (setv ___ab_cd? "tacos") - (assert (= ___ab_cd? "tacos")) - (assert (= ___is_ab_cd "tacos"))) + (assert (= hyx_fooXquestion_markX "nachos"))) (defn test-py-forbidden-ascii [] @@ -71,10 +67,7 @@ (assert (= (+ hyx_XflowerXab hyx_Xblack_heart_suitX) "flowerlove")) (setv ⚘-⚘ "doubleflower") (assert (= ⚘-⚘ "doubleflower")) - (assert (= hyx_XflowerX_XflowerX "doubleflower")) - (setv ⚘? "mystery") - (assert (= ⚘? "mystery")) - (assert (= hyx_is_XflowerX "mystery"))) + (assert (= hyx_XflowerX_XflowerX "doubleflower"))) (defn test-higher-unicode [] @@ -109,14 +102,18 @@ (assert (= x "aabb"))) -(defreader rm---x +(defreader rm--- (setv form (.parse-one-form &reader)) - [form form]) + `(do (+= ~form "a") + ~form)) +(defreader rm___ + (setv form (.parse-one-form &reader)) + `(do (+= ~form "b") + ~form)) (defn test-reader-macro [] (setv x "") - (assert (= #rm---x (do (+= x "a") 1) [1 1])) - (assert (= #rm___x (do (+= x "b") 2) [2 2])) - (assert (= x "aabb"))) + (assert (= #rm--- x "a")) + (assert (= #rm___ x "ab"))) (defn test-special-form [] @@ -152,34 +149,33 @@ (defn test-keyword-args [] - (defn f [a a-b foo? ☘] - [a a-b foo? ☘]) - (assert (= (f :foo? 3 :☘ 4 :a 1 :a-b 2) [1 2 3 4])) - (assert (= (f :is_foo 3 :hyx_XshamrockX 4 :a 1 :a_b 2) [1 2 3 4])) + (defn f [a a-b ☘] + [a a-b ☘]) + (assert (= (f :☘ 3 :a 1 :a-b 2) [1 2 3])) + (assert (= (f :hyx_XshamrockX 3 :a 1 :a_b 2) [1 2 3])) (defn g [#** x] x) - (assert (= (g :foo? 3 :☘ 4 :a 1 :a-b 2) - {"a" 1 "a_b" 2 "is_foo" 3 "hyx_XshamrockX" 4})) - (assert (= (g :is_foo 3 :hyx_XshamrockX 4 :a 1 :a_b 2) - {"a" 1 "a_b" 2 "is_foo" 3 "hyx_XshamrockX" 4}))) + (assert (= (g :☘ 3 :a 1 :a-b 2) + {"a" 1 "a_b" 2 "hyx_XshamrockX" 3})) + (assert (= (g :hyx_XshamrockX 3 :a 1 :a_b 2) + {"a" 1 "a_b" 2 "hyx_XshamrockX" 3}))) (defn test-late-mangling [] ; Mangling should only happen during compilation. - (assert (!= 'foo? 'is_foo)) - (setv sym 'foo?) - (assert (= sym (hy.models.Symbol "foo?"))) - (assert (!= sym (hy.models.Symbol "is_foo"))) + (assert (!= 'foo-bar 'foo_bar)) + (setv sym 'foo-bar) + (assert (= sym (hy.models.Symbol "foo-bar"))) + (assert (!= sym (hy.models.Symbol "foo_bar"))) (setv out (hy.eval `(do (setv ~sym 10) - [foo? is_foo]))) + [foo-bar foo_bar]))) (assert (= out [10 10]))) (defn test-functions [] - (for [[a b] [["___ab-cd?" "___is_ab_cd"] - ["⚘-⚘" "hyx_XflowerX_XflowerX"]]] + (for [[a b] [["⚘-⚘" "hyx_XflowerX_XflowerX"]]] (assert (= (hy.mangle a) b)) (assert (= (hy.unmangle b) a)))) diff --git a/tests/native_tests/py3_10_only_tests.hy b/tests/native_tests/match.hy similarity index 95% rename from tests/native_tests/py3_10_only_tests.hy rename to tests/native_tests/match.hy index d37d70d7f..152500638 100644 --- a/tests/native_tests/py3_10_only_tests.hy +++ b/tests/native_tests/match.hy @@ -1,7 +1,14 @@ +(do-mac (when hy._compat.PY3_10 '(do + + (import pytest dataclasses [dataclass] hy.errors [HySyntaxError]) +(defclass [dataclass] Point [] + (#^ int x) + (#^ int y)) + (defn test-pattern-matching [] (assert (is (match 0 0 :if False False @@ -75,10 +82,6 @@ 0) 0)) - (defclass [dataclass] Point [] - (#^int x) - (#^int y)) - (assert (= 0 (match (Point 1 0) (Point 1 :y var) var))) (assert (is None (match (Point 0 0) (Point 1 :y var) var))) @@ -127,6 +130,12 @@ (hy.eval '(match 1 1 :if True :as x x)))) +(defn test-dotted-constructor [] + ; https://github.com/hylang/hy/issues/2404 + (defclass C [Point] + (setv C Point)) + (assert (= (match (Point 1 2) (C.C 1 2) "ok") "ok"))) + (defn test-matching-side-effects [] (setv x 0) (defn foo [] @@ -162,10 +171,6 @@ (assert (= [x y] [5 6]))) (assert (= [x y] [3 4]))) -(defclass [dataclass] Point [] - (#^int x) - (#^int y)) - (defn test-let-match-pattern [] (setv [x y] [1 2] p (Point 5 6)) @@ -255,8 +260,11 @@ _ _))) (assert (= {"a" 1 "c" 3} (match y - {"b" b #**e} e))) + {"b" b #** e} e))) (assert (= {"a" 1 "c" 3} (match y - {"b" b #**a} a))) + {"b" b #** a} a))) (assert (= b 2)))) + + +))) diff --git a/tests/native_tests/model_patterns.hy b/tests/native_tests/model_patterns.hy index 5ca493db1..8aa5e6ee7 100644 --- a/tests/native_tests/model_patterns.hy +++ b/tests/native_tests/model_patterns.hy @@ -39,7 +39,6 @@ (defn f [loopers] (setv head (if loopers (get loopers 0) None)) (setv tail (cut loopers 1 None)) - (print head) (cond (is head None) `(do ~@body) @@ -66,5 +65,4 @@ for n from 1 to 3 for p in [k n (* 10 n)] do (.append l p) (-= k 1)) - (print l) (assert (= l [2 1 10 -1 2 20 -4 3 30]))) diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy deleted file mode 100644 index a5a0aa40f..000000000 --- a/tests/native_tests/native_macros.hy +++ /dev/null @@ -1,380 +0,0 @@ -(import os sys - importlib - pytest - hy.errors [HySyntaxError HyTypeError HyMacroExpansionError]) - -(defmacro rev [#* body] - "Execute the `body` statements in reverse" - (quasiquote (do (unquote-splice (list (reversed body)))))) - - -(defn test-stararged-native-macro [] - (setv x []) - (rev (.append x 1) (.append x 2) (.append x 3)) - (assert (= x [3 2 1]))) - -(defn test-macros-returning-constants [] - (defmacro an-int [] 42) - (assert (= (an-int) 42)) - - (defmacro a-true [] True) - (assert (= (a-true) True)) - (defmacro a-false [] False) - (assert (= (a-false) False)) - - (defmacro a-float [] 42.) - (assert (= (a-float) 42.)) - - (defmacro a-complex [] 42j) - (assert (= (a-complex) 42j)) - - (defmacro a-string [] "foo") - (assert (= (a-string) "foo")) - - (defmacro a-bytes [] b"foo") - (assert (= (a-bytes) b"foo")) - - (defmacro a-list [] [1 2]) - (assert (= (a-list) [1 2])) - - (defmacro a-tuple [#* b] b) - (assert (= (a-tuple 1 2) #(1 2))) - - (defmacro a-dict [] {1 2}) - (assert (= (a-dict) {1 2})) - - (defmacro a-set [] #{1 2}) - (assert (= (a-set) #{1 2})) - - (defmacro a-none []) - (assert (= (a-none) None))) - - -; A macro calling a previously defined function -(eval-when-compile - (defn foo [x y] - (quasiquote (+ (unquote x) (unquote y))))) - -(defmacro bar [x y] - (foo x y)) - -(defn test-macro-kw [] - "An error is raised when * or #** is used in a macro" - - (with [(pytest.raises HySyntaxError)] - (hy.eval '(defmacro f [* a b]))) - - (with [(pytest.raises HySyntaxError)] - (hy.eval '(defmacro f [#** kw]))) - - (with [(pytest.raises HySyntaxError)] - (hy.eval '(defmacro f [a b #* body c])))) - -(defn test-macro-bad-name [] - (with [e (pytest.raises HySyntaxError)] - (hy.eval '(defmacro :kw []))) - (assert (in "got unexpected token: :kw" e.value.msg)) - - (with [e (pytest.raises HySyntaxError)] - (hy.eval '(defmacro foo.bar []))) - (assert (in "periods are not allowed in macro names" e.value.msg))) - -(defn test-macro-calling-fn [] - (assert (= 3 (bar 1 2)))) - -(defn test-optional-and-unpacking-in-macro [] - ; https://github.com/hylang/hy/issues/1154 - (defn f [#* args] - (+ "f:" (repr args))) - (defmacro mac [[x None]] - `(f #* [~x])) - (assert (= (mac) "f:(None,)"))) - -(defn test-macro-autoboxing-docstring [] - (defmacro m [] - (setv mystring "hello world") - `(fn [] ~mystring (+ 1 2))) - (setv f (m)) - (assert (= (f) 3)) - (assert (= f.__doc__ "hello world"))) - -(defn test-midtree-yield [] - "Test yielding with a returnable." - (defn kruft [] (yield) (+ 1 1))) - -(defn test-midtree-yield-in-for [] - "Test yielding in a for with a return." - (defn kruft-in-for [] - (for [i (range 5)] - (yield i)) - (+ 1 2))) - -(defn test-midtree-yield-in-while [] - "Test yielding in a while with a return." - (defn kruft-in-while [] - (setv i 0) - (while (< i 5) - (yield i) - (setv i (+ i 1))) - (+ 2 3))) - -(defn test-multi-yield [] - (defn multi-yield [] - (for [i (range 3)] - (yield i)) - (yield "a") - (yield "end")) - (assert (= (list (multi-yield)) [0 1 2 "a" "end"]))) - - -; Macro that checks a variable defined at compile or load time -(setv phase "load") -(eval-when-compile - (setv phase "compile")) -(defmacro phase-when-compiling [] phase) -(assert (= phase "load")) -(assert (= (phase-when-compiling) "compile")) - -(setv initialized False) -(eval-and-compile - (setv initialized True)) -(defmacro test-initialized [] initialized) -(assert initialized) -(assert (test-initialized)) - -(defn test-gensym-in-macros [] - (import ast) - (import hy.compiler [hy-compile]) - (import hy.reader [read-many]) - (setv macro1 "(defmacro nif [expr pos zero neg] - (setv g (hy.gensym)) - `(do - (setv ~g ~expr) - (cond (> ~g 0) ~pos - (= ~g 0) ~zero - (< ~g 0) ~neg))) - - (print (nif (inc -1) 1 0 -1)) - ") - ;; expand the macro twice, should use a different - ;; gensym each time - (setv _ast1 (hy-compile (read-many macro1) __name__)) - (setv _ast2 (hy-compile (read-many macro1) __name__)) - (setv s1 (ast.unparse _ast1)) - (setv s2 (ast.unparse _ast2)) - ;; and make sure there is something new that starts with _G\uffff - (assert (in (hy.mangle "_G\uffff") s1)) - (assert (in (hy.mangle "_G\uffff") s2)) - ;; but make sure the two don't match each other - (assert (not (= s1 s2)))) - - -(defn test-macro-namespace-resolution [] - "Confirm that local versions of macro-macro dependencies do not shadow the -versions from the macro's own module, but do resolve unbound macro references -in expansions." - - ;; `nonlocal-test-macro` is a macro used within - ;; `tests.resources.macro-with-require.test-module-macro`. - ;; Here, we introduce an equivalently named version in local scope that, when - ;; used, will expand to a different output string. - (defmacro nonlocal-test-macro [x] - (print "this is the local version of `nonlocal-test-macro`!")) - - ;; Was the above macro created properly? - (assert (in "nonlocal_test_macro" __macros__)) - - (setv nonlocal-test-macro (get __macros__ "nonlocal_test_macro")) - - (require tests.resources.macro-with-require *) - - (setv module-name-var "tests.native_tests.native_macros.test-macro-namespace-resolution") - (assert (= (+ "This macro was created in tests.resources.macros, " - "expanded in tests.native_tests.native_macros.test-macro-namespace-resolution " - "and passed the value 2.") - (test-module-macro 2))) - - ;; Now, let's use a `require`d macro that depends on another macro defined only - ;; in this scope. - (defmacro local-test-macro [x] - (.format "This is the local version of `nonlocal-test-macro` returning {}!" (int x))) - - (assert (= "This is the local version of `nonlocal-test-macro` returning 3!" - (test-module-macro-2 3)))) - -(defn test-requires-pollutes-core [] - ;; https://github.com/hylang/hy/issues/1978 - ;; Macros loaded from an external module should not pollute `__macros__` - ;; with macros from core. - - (setv pyc-file (importlib.util.cache-from-source - (os.path.realpath - (os.path.join - "tests" "resources" "macros.hy")))) - - ;; Remove any cached byte-code, so that this runs from source and - ;; gets evaluated in this module. - (when (os.path.isfile pyc-file) - (os.unlink pyc-file) - (.clear sys.path_importer_cache) - (when (in "tests.resources.macros" sys.modules) - (del (get sys.modules "tests.resources.macros")) - (__macros__.clear))) - - ;; Ensure that bytecode isn't present when we require this module. - (assert (not (os.path.isfile pyc-file))) - - (defn require-macros [] - (require tests.resources.macros :as m) - - (assert (in (hy.mangle "m.test-macro") __macros__)) - (for [macro-name __macros__] - (assert (not (and (in "with" macro-name) - (!= "with" macro-name)))))) - - (require-macros) - - ;; Now that bytecode is present, reload the module, clear the `require`d - ;; macros and tags, and rerun the tests. - (assert (os.path.isfile pyc-file)) - - ;; Reload the module and clear the local macro context. - (.clear sys.path_importer_cache) - (del (get sys.modules "tests.resources.macros")) - (.clear __macros__) - - (require-macros)) - -(defn [(pytest.mark.xfail)] test-macro-from-module [] - " - Macros loaded from an external module, which itself `require`s macros, should - work without having to `require` the module's macro dependencies (due to - [minimal] macro namespace resolution). - - In doing so we also confirm that a module's `__macros__` attribute is correctly - loaded and used. - - Additionally, we confirm that `require` statements are executed via loaded bytecode. - " - - (setv pyc-file (importlib.util.cache-from-source - (os.path.realpath - (os.path.join - "tests" "resources" "macro_with_require.hy")))) - - ;; Remove any cached byte-code, so that this runs from source and - ;; gets evaluated in this module. - (when (os.path.isfile pyc-file) - (os.unlink pyc-file) - (.clear sys.path_importer_cache) - (when (in "tests.resources.macro_with_require" sys.modules) - (del (get sys.modules "tests.resources.macro_with_require")) - (__macros__.clear))) - - ;; Ensure that bytecode isn't present when we require this module. - (assert (not (os.path.isfile pyc-file))) - - (defn test-requires-and-macros [] - (require tests.resources.macro-with-require - [test-module-macro]) - - ;; Make sure that `require` didn't add any of its `require`s - (assert (not (in (hy.mangle "nonlocal-test-macro") __macros__))) - ;; and that it didn't add its tags. - (assert (not (in (hy.mangle "#test-module-tag") __macros__))) - - ;; Now, require everything. - (require tests.resources.macro-with-require *) - - ;; Again, make sure it didn't add its required macros and/or tags. - (assert (not (in (hy.mangle "nonlocal-test-macro") __macros__))) - - ;; Its tag(s) should be here now. - (assert (in (hy.mangle "#test-module-tag") __macros__)) - - ;; The test macro expands to include this symbol. - (setv module-name-var "tests.native_tests.native_macros") - (assert (= (+ "This macro was created in tests.resources.macros, " - "expanded in tests.native_tests.native_macros " - "and passed the value 1.") - (test-module-macro 1)))) - - (test-requires-and-macros) - - ;; Now that bytecode is present, reload the module, clear the `require`d - ;; macros and tags, and rerun the tests. - (assert (os.path.isfile pyc-file)) - - ;; Reload the module and clear the local macro context. - (.clear sys.path_importer_cache) - (del (get sys.modules "tests.resources.macro_with_require")) - (.clear __macros__) - - ;; There doesn't seem to be a way--via standard import mechanisms--to - ;; ensure that an imported module used the cached bytecode. We'll simply have - ;; to trust that the .pyc loading convention was followed. - (test-requires-and-macros)) - - -(defn test-recursive-require-star [] - "(require foo *) should pull in macros required by `foo`." - (require tests.resources.macro-with-require *) - - (test-macro) - (assert (= blah 1))) - - -(defn test-macro-errors [] - (import traceback - hy.importer [read-many]) - - (setv test-expr (read-many "(defmacro blah [x] `(print ~@z)) (blah y)")) - - (with [excinfo (pytest.raises HyMacroExpansionError)] - (hy.eval test-expr)) - - (setv output (traceback.format_exception_only - excinfo.type excinfo.value)) - (setv output (cut (.splitlines (.strip (get output 0))) 1 None)) - - (setv expected [" File \"\", line 1" - " (defmacro blah [x] `(print ~@z)) (blah y)" - " ^------^" - "expanding macro blah" - " NameError: global name 'z' is not defined"]) - - (assert (= (cut expected 0 -1) (cut output 0 -1))) - (assert (or (= (get expected -1) (get output -1)) - ;; Handle PyPy's peculiarities - (= (.replace (get expected -1) "global " "") (get output -1)))) - - - ;; This should throw a `HyWrapperError` that gets turned into a - ;; `HyMacroExpansionError`. - (with [excinfo (pytest.raises HyMacroExpansionError)] - (hy.eval '(do (defmacro wrap-error-test [] - (fn [])) - (wrap-error-test)))) - (assert (in "HyWrapperError" (str excinfo.value)))) - -(defn test-delmacro - [] - ;; test deletion of user defined macro - (defmacro delete-me [] "world") - (delmacro delete-me) - (with [exc (pytest.raises NameError)] - (delete-me)) - ;; test deletion of required macros - (require tests.resources.tlib [qplah parald]) - (assert (and (qplah 1) (parald 1))) - - (delmacro qplah parald) - (with [exc (pytest.raises NameError)] - (hy.eval '(qplah))) - (with [exc (pytest.raises NameError)] - (hy.eval '(parald)))) - -(defn test-macro-redefinition-warning - [] - (with [(pytest.warns RuntimeWarning :match "require already refers to")] - (hy.eval '(defmacro require [] 1)))) diff --git a/tests/native_tests/nonlocal.hy b/tests/native_tests/nonlocal.hy new file mode 100644 index 000000000..4bef411a5 --- /dev/null +++ b/tests/native_tests/nonlocal.hy @@ -0,0 +1,48 @@ +(import pytest) + + +(defn test-nonlocal-promotion [] + (setv G {}) + (hy.eval '(do + (setv home "earth") + (defn blastoff [] + (nonlocal home) + (setv home "saturn")) + (blastoff)) + :globals G) + (assert (= (get G "home") "saturn")) + + (setv health + (hy.eval '(do + (defn make-ration-log [days intensity] + (setv health 20 + ration-log + (list (map (fn [_] + ;; only `rations` should be upgraded + (nonlocal rations health) + (-= rations intensity) + (+= health (* 0.5 intensity)) + rations) + (range days)))) + health) + ;; "late" global binding should still work + (setv rations 100) + (make-ration-log 43 1.5)) + :globals G)) + (assert (= health (+ 20 (* 43 0.5 1.5)))) + (assert (= (get G "rations") (- 100 (* 43 1.5))))) + + +(defn test-nonlocal-must-have-def [] + (with [err (pytest.raises SyntaxError)] + (hy.eval '(do + (defn make-ration-log [days intensity] + (list (map (fn [_] + (nonlocal rations) + (-= rations intensity) + rations) + (range days)))) + ;; oops! I forgot to pack my rations! + (make-ration-log 43 1.5)) + :globals {})) + (assert (in "no binding for nonlocal 'rations'" err.value.msg))) diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy index 0d2618bc1..6e88ab027 100644 --- a/tests/native_tests/operators.hy +++ b/tests/native_tests/operators.hy @@ -158,7 +158,7 @@ ; so we don't allow `^` with 3 arguments, either. -(op-and-shadow-test ~ +(op-and-shadow-test bnot (forbid (f)) (assert (= (& (f 0b00101111) 0xFF) 0b11010000)) @@ -314,6 +314,31 @@ (assert (= (f [[1 2 3] [4 5 6] [7 8 9]] 1 2) 6)) (assert (= (f {"x" {"y" {"z" 12}}} "x" "y" "z") 12))) +(defn test-setv-get [] + (setv foo [0 1 2]) + (setv (get foo 0) 12) + (assert (= (get foo 0) 12))) + + +(op-and-shadow-test [cut] + (forbid (f)) + (setv x "abcdef") + (assert (= (f x) "abcdef")) + (assert (= (f x 3) "abc")) + (assert (= (f x -2) "abcd")) + (assert (= (f x 3 None) "def")) + (assert (= (f x -2 None) "ef")) + (assert (= (f x 3 5) "de")) + (assert (= (f x 0 None 2) "ace")) + (assert (= (list (f (range 100) 20 80 13)) [20 33 46 59 72]))) + +(defn test-setv-cut [] + (setv foo (list (range 20))) + (setv (cut foo 2 18 3) (* [0] 6)) + (assert (= + foo + [0 1 0 3 4 0 6 7 0 9 10 0 12 13 0 15 16 0 18 19]))) + (defn test-chained-comparison [] (assert (chainc 2 = (+ 1 1) = (- 3 1))) diff --git a/tests/native_tests/other.hy b/tests/native_tests/other.hy new file mode 100644 index 000000000..88bdae132 --- /dev/null +++ b/tests/native_tests/other.hy @@ -0,0 +1,52 @@ +;; Tests miscellaneous features of the language not covered elsewhere + +(import + typing [get-type-hints] + pytest + hy.errors [HyLanguageError]) + + +(defn test-illegal-assignments [] + (for [form '[ + (setv (do 1 2) 1) + (setv 1 1) + (setv {1 2} 1) + (del 1 1) + ; https://github.com/hylang/hy/issues/1780 + (setv None 1) + (setv False 1) + (setv True 1) + (defn None [] (print "hello")) + (defn True [] (print "hello")) + (defn f [True] (print "hello")) + (for [True [1 2 3]] (print "hello")) + (lfor True [1 2 3] True) + (lfor :setv True 1 True) + (with [True x] (print "hello")) + (try 1 (except [True AssertionError] 2)) + (defclass True [])]] + (with [e (pytest.raises HyLanguageError)] + (hy.eval form)) + (assert (in "Can't assign" e.value.msg)))) + + +(defn test-no-str-as-sym [] + "Don't treat strings as symbols in the calling position" + (with [(pytest.raises TypeError)] ("setv" True 3)) ; A special form + (with [(pytest.raises TypeError)] ("abs" -2)) ; A function + (with [(pytest.raises TypeError)] ("when" 1 2))) ; A macro + + +(defn test-undefined-name [] + (with [(pytest.raises NameError)] + xxx)) + + +(defn test-variable-annotations [] + (defclass AnnotationContainer [] + (setv #^ int x 1 y 2) + (#^ bool z)) + + (setv annotations (get-type-hints AnnotationContainer)) + (assert (= (get annotations "x") int)) + (assert (= (get annotations "z") bool))) diff --git a/tests/native_tests/quote.hy b/tests/native_tests/quote.hy index 0f874c691..2d2771228 100644 --- a/tests/native_tests/quote.hy +++ b/tests/native_tests/quote.hy @@ -1,85 +1,163 @@ -(defn test-quote [] - (setv q (quote (a b c))) - (assert (= (len q) 3)) - (assert (= q (hy.models.Expression [(quote a) (quote b) (quote c)])))) +"Tests of `quote` and `quasiquote`." + +(import + pytest) -(defn test-basic-quoting [] - (assert (= (type (quote (foo bar))) hy.models.Expression)) - (assert (= (type (quote foo)) hy.models.Symbol)) - (assert (= (type (quote "string")) hy.models.String)) - (assert (= (type (quote b"string")) hy.models.Bytes))) +(setv E hy.models.Expression) +(setv S hy.models.Symbol) + + +(defn test-quote-basic [] + (assert (= '3 (hy.models.Integer 3))) + (assert (= 'a (S "a"))) + (assert (= 'False (S "False"))) + (assert (= '"hello" (hy.models.String "hello"))) + (assert (= 'b"hello" (hy.models.Bytes b"hello"))) + (assert (= '(a b) (E [(S "a") (S "b")]))) + (assert (= + '{foo bar baz quux} + (hy.models.Dict (map S ["foo" "bar" "baz" "quux"]))))) (defn test-quoted-hoistable [] - (setv f (quote (if True True True))) - (assert (= (get f 0) (quote if))) - (assert (= (cut f 1 None) (quote (True True True))))) + (setv f '(if True True True)) + (assert (= (get f 0) 'if)) + (assert (= (cut f 1 None) '(True True True)))) + + +(defn test-quasiquote-trivial [] + "Quasiquote and quote are equivalent for simple cases." + (assert (= `(a b c) '(a b c)))) (defn test-quoted-macroexpand [] "Don't expand macros in quoted expressions." (require tests.resources.macros [test-macro]) - (setv q1 (quote (test-macro))) - (setv q2 (quasiquote (test-macro))) + (setv q1 '(test-macro)) + (setv q2 `(test-macro)) (assert (= q1 q2)) - (assert (= (get q1 0) (quote test-macro)))) - - -(defn test-quote-dicts [] - (setv q (quote {foo bar baz quux})) - (assert (= (len q) 4)) - (assert (= (get q 0) (quote foo))) - (assert (= (get q 1) (quote bar))) - (assert (= (get q 2) (quote baz))) - (assert (= (get q 3) (quote quux))) - (assert (= (type q) hy.models.Dict))) + (assert (= (get q1 0) 'test-macro))) (defn test-quote-expr-in-dict [] - (setv q (quote {(foo bar) 0})) - (assert (= (len q) 2)) - (setv qq (get q 0)) - (assert (= qq (quote (foo bar))))) - - -(defn test-quasiquote [] - "Quasiquote and quote are equivalent for simple cases." - (setv q (quote (a b c))) - (setv qq (quasiquote (a b c))) - (assert (= q qq))) + (assert (= + '{(foo bar) 0} + (hy.models.Dict [(E [(S "foo") (S "bar")]) (hy.models.Integer 0)])))) (defn test-unquote [] - (setv q (quote (unquote foo))) + (setv q '~foo) (assert (= (len q) 2)) - (assert (= (get q 1) (quote foo))) - (setv qq (quasiquote (a b c (unquote (+ 1 2))))) - (assert (= (len qq) 4)) - (assert (= (hy.as-model qq) (quote (a b c 3))))) + (assert (= (get q 1) 'foo)) + (setv qq `(a b c ~(+ 1 2))) + (assert (= (hy.as-model qq) '(a b c 3)))) (defn test-unquote-splice [] - (setv q (quote (c d e))) + (setv q '(c d e)) (setv qq `(a b ~@q f ~@q ~@0 ~@False ~@None g ~@(when False 1) h)) - (assert (= (len qq) 11)) - (assert (= qq (quote (a b c d e f c d e g h))))) + (assert (= qq '(a b c d e f c d e g h)))) + + +(defmacro doodle [#* args] + `[1 ~@args 2]) + +(defn test-unquote-splice-in-mac [] + (assert (= + (doodle + (setv x 5) + (+= x 1) + x) + [1 None None 6 2]))) + + +(defn test-unquote-splice-unpack [] + ; https://github.com/hylang/hy/issues/2336 + (with [(pytest.raises hy.errors.HySyntaxError)] + (hy.eval '`[~@ #* [[1]]]))) (defn test-nested-quasiquote [] (setv qq (hy.as-model `(1 `~(+ 1 ~(+ 2 3) ~@None) 4))) - (setv q (quote (1 `~(+ 1 5) 4))) + (setv q '(1 `~(+ 1 5) 4)) (assert (= (len q) 3)) - (assert (= (get qq 1) (quote `~(+ 1 5)))) - (assert (= q qq))) + (assert (= (get qq 1) '`~(+ 1 5))) + (assert (= qq q))) -(defmacro doodle [#* body] - `(do ~@body)) - -(defn test-unquote-splice [] +(defn test-nested-quasiquote--nested-struct [] (assert (= - (doodle - [1 2 3] - [4 5 6]) - [4 5 6]))) + (hy.as-model `(try + ~@(lfor + i [1 2 3] + `(setv ~(S (+ "x" (str i))) (+ "x" (str ~i)))) + (finally + (print "done")))) + '(try + (setv x1 (+ "x" (str 1))) + (setv x2 (+ "x" (str 2))) + (setv x3 (+ "x" (str 3))) + (finally + (print "done")))))) + + +(defmacro macroify-programs [#* names] + `(do + ~@(lfor name names + `(defmacro ~name [#* args] + `(.append imaginary-syscalls #(~'~(str name) ~@(map str args))))))) + +(defn test-nested-quasiquote--macro-writing-macro-1 [] + "A test of the construct ~'~ (to substitute in a variable from a + higher-level quasiquote) inspired by + https://github.com/hylang/hy/discussions/2251" + + (setv imaginary-syscalls []) + (macroify-programs ls mkdir touch) + (mkdir mynewdir) + (touch mynewdir/file) + (ls -lA mynewdir) + (assert (= imaginary-syscalls [ + #("mkdir" "mynewdir") + #("touch" "mynewdir/file") + #("ls" "-lA" "mynewdir")]))) + + +(defmacro def-caller [abbrev proc] + `(defmacro ~abbrev [var form] + `(~'~proc + (fn [~var] ~form)))) +(def-caller smoo-caller smoo) + +(defn test-nested-quasiquote--macro-writing-macro-2 [] + "A similar test to the previous one, based on section 3.2 of + Bawden, A. (1999). Quasiquotation in Lisp. ACM SIGPLAN Workshop on Partial Evaluation and Program Manipulation. Retrieved from http://web.archive.org/web/20230105083805id_/https://3e8.org/pub/scheme/doc/Quasiquotation%20in%20Lisp%20(Bawden).pdf" + + (setv accum []) + (defn smoo [f] + (.append accum "entered smoo") + (f "in smoo") + (.append accum "exiting smoo")) + (smoo-caller arg + (.append accum (+ "in the lambda: " arg))) + (assert (= accum [ + "entered smoo" + "in the lambda: in smoo" + "exiting smoo"]))) + + +(defn test-nested-quasiquote--triple [] + "N.B. You can get the same results with an analogous test in Emacs + Lisp or Common Lisp." + + (setv + a 1 b 1 c 1 + x ```[~a ~~b ~~~c] + ; `x` has been implicitly evaluated once. Let's explicitly + ; evaluate it twice more, so no backticks are left. + a 2 b 2 c 2 + x (hy.eval x) + a 3 b 3 c 3 + x (hy.eval x)) + (assert (= (hy.as-model x) '[3 2 1]))) diff --git a/tests/native_tests/reader_macros.hy b/tests/native_tests/reader_macros.hy index 5552947b8..cabd39dd9 100644 --- a/tests/native_tests/reader_macros.hy +++ b/tests/native_tests/reader_macros.hy @@ -4,6 +4,8 @@ types contextlib [contextmanager] hy.errors [HyMacroExpansionError] + hy.reader [HyReader] + hy.reader.exceptions [PrematureEndOfInput] pytest) @@ -30,8 +32,14 @@ (defn test-reader-macros [] (assert (= (eval-module #[[(defreader foo '1) #foo]]) 1)) - (assert (in (hy.mangle "#foo") - (eval-module #[[(defreader foo '1) __reader_macros__]]))) + (assert (in "foo" + (eval-module #[[(defreader foo '1) _hy_reader_macros]]))) + (assert (= (eval-module #[[(defreader ^foo '1) #^foo]]) 1)) + + (assert (not-in "rm___x" + (eval-module + #[[(defreader rm---x '1) + _hy_reader_macros]]))) ;; Assert reader macros operating exclusively at read time (with [module (temp-module "")] @@ -47,39 +55,99 @@ (defn test-bad-reader-macro-name [] (with [(pytest.raises HyMacroExpansionError)] (eval-module "(defreader :a-key '1)")) + (with [(pytest.raises PrematureEndOfInput)] + (eval-module "# _ 3"))) - (with [(pytest.raises HyMacroExpansionError)] - (eval-module "(defreader ^foo '1)"))) +(defn test-get-macro [] + (assert (eval-module #[[ + (defreader rm1 + 11) + (defreader rm☘ + 22) + (and + (is (get-macro :reader rm1) (get _hy_reader_macros "rm1")) + (is (get-macro :reader rm☘) (get _hy_reader_macros "rm☘")))]]))) + +(defn test-docstring [] + (assert (= + (eval-module #[[ + (defreader foo + "docstring of foo" + 15) + #(#foo (. (get-macro :reader foo) __doc__))]]) + #(15 "docstring of foo")))) (defn test-require-readers [] (with [module (temp-module "")] - (setv it (hy.read-many #[[(require tests.resources.tlib :readers [upper]) - #upper hello]])) + (setv it (hy.read-many #[[(require tests.resources.tlib :readers [upper!]) + #upper! hello]])) (eval-isolated (next it) module) (assert (= (next it) 'HELLO))) ;; test require :readers & :macros is order independent - (for [s ["[qplah] :readers [upper]" - ":readers [upper] [qplah]" - ":macros [qplah] :readers [upper]" - ":readers [upper] :macros [qplah]"]] + (for [s ["[qplah] :readers [upper!]" + ":readers [upper!] [qplah]" + ":macros [qplah] :readers [upper!]" + ":readers [upper!] :macros [qplah]"]] (assert (= (eval-module #[f[ (require tests.resources.tlib {s}) - [(qplah 1) #upper "hello"]]f]) + [(qplah 1) #upper! "hello"]]f]) [[8 1] "HELLO"]))) ;; test require :readers * (assert (= (eval-module #[=[ (require tests.resources.tlib :readers *) - [#upper "eVeRy" #lower "ReAdEr"]]=]) + [#upper! "eVeRy" #lower "ReAdEr"]]=]) ["EVERY" "reader"])) ;; test can't redefine :macros or :readers assignment brackets (with [(pytest.raises hy.errors.HySyntaxError)] - (eval-module #[[(require tests.resources.tlib [taggart] [upper])]])) + (eval-module #[[(require tests.resources.tlib [taggart] [upper!])]])) (with [(pytest.raises hy.errors.HySyntaxError)] - (eval-module #[[(require tests.resources.tlib :readers [taggart] :readers [upper])]])) + (eval-module #[[(require tests.resources.tlib :readers [taggart] :readers [upper!])]])) (with [(pytest.raises hy.errors.HyRequireError)] (eval-module #[[(require tests.resources.tlib :readers [not-a-real-reader])]]))) + +(defn test-eval-read [] + ;; https://github.com/hylang/hy/issues/2291 + ;; hy.eval should not raise an exception when + ;; defining readers using hy.read or with quoted forms + (with [module (temp-module "")] + (hy.eval (hy.read "(defreader r 5)") :module module) + (hy.eval '(defreader test-read 4) :module module) + (hy.eval '(require tests.resources.tlib :readers [upper!]) :module module) + ;; these reader macros should not exist in any current reader + (for [tag #("#r" "#test-read" "#upper!")] + (with [(pytest.raises hy.errors.HySyntaxError)] + (hy.read tag))) + ;; but they should be installed in the module + (hy.eval '(setv reader (hy.reader.HyReader :use-current-readers True)) :module module) + (setv reader module.reader) + (for [[s val] [["#r" 5] + ["#test-read" 4] + ["#upper! \"hi there\"" "HI THERE"]]] + (assert (= (hy.eval (hy.read s :reader reader) :module module) val)))) + + ;; passing a reader explicitly should work as expected + (with [module (temp-module "")] + (setv reader (HyReader)) + (defn eval1 [s] + (hy.eval (hy.read s :reader reader) :module module)) + (eval1 "(defreader fbaz 32)") + (eval1 "(require tests.resources.tlib :readers [upper!])") + (assert (= (eval1 "#fbaz") 32)) + (assert (= (eval1 "#upper! \"hello\"") "HELLO")))) + + +(defn test-interleaving-readers [] + (with [module1 (temp-module "") + module2 (temp-module "")] + (setv stream1 (hy.read-many #[[(do (defreader foo "foo1") (defreader bar "bar1")) #foo #bar]]) + stream2 (hy.read-many #[[(do (defreader foo "foo2") (defreader bar "bar2")) #foo #bar]]) + valss [[None None] ["foo1" "foo2"] ["bar1" "bar2"]]) + (for [[form1 form2 vals] (zip stream1 stream2 valss)] + (assert (= vals + [(hy.eval form1 :module module1) + (hy.eval form2 :module module2)]))))) diff --git a/tests/native_tests/repl.hy b/tests/native_tests/repl.hy new file mode 100644 index 000000000..570f2e565 --- /dev/null +++ b/tests/native_tests/repl.hy @@ -0,0 +1,186 @@ +; Some other tests of the REPL are in `test_bin.py`. + +(import + io + sys + re + pytest) + + +(defn [pytest.fixture] rt [monkeypatch capsys] + "Do a test run of the REPL." + (fn [[inp ""] [to-return 'out] [spy False] [py-repr False]] + (monkeypatch.setattr "sys.stdin" (io.StringIO inp)) + (.run (hy.REPL + :spy spy + :output-fn (when py-repr repr))) + (setv result (capsys.readouterr)) + (cond + (= to-return 'out) result.out + (= to-return 'err) result.err + (= to-return 'both) result))) + +(defmacro has [haystack needle] + "`in` backwards." + `(in ~needle ~haystack)) + + +(defn test-simple [rt] + (assert (has (rt #[[(.upper "hello")]]) "HELLO"))) + +(defn test-spy [rt] + (setv x (rt #[[(.upper "hello")]] :spy True)) + (assert (has x ".upper()")) + (assert (has x "HELLO")) + ; `spy` should work even when an exception is thrown + (assert (has (rt "(foof)" :spy True) "foof()"))) + +(defn test-multiline [rt] + (assert (has (rt "(+ 1 3\n5 9)") " 18\n=> "))) + +(defn test-history [rt] + (assert (has + (rt #[[ + (+ "a" "b") + (+ "c" "d") + (+ "e" "f") + (.format "*1: {}, *2: {}, *3: {}," *1 *2 *3)]]) + #[["*1: ef, *2: cd, *3: ab,"]])) + (assert (has + (rt #[[ + (raise (Exception "TEST ERROR")) + (+ "err: " (str *e))]]) + #[["err: TEST ERROR"]]))) + +(defn test-comments [rt] + (setv err-empty (rt "" 'err)) + (setv x (rt #[[(+ "a" "b") ; "c"]] 'both)) + (assert (has x.out "ab")) + (assert (= x.err err-empty)) + (assert (= (rt "; 1" 'err) err-empty))) + +(defn test-assignment [rt] + "If the last form is an assignment, don't print the value." + (assert (not (has (rt #[[(setv x (+ "A" "Z"))]]) "AZ"))) + (setv x (rt #[[(setv x (+ "A" "Z")) (+ "B" "Y")]])) + (assert (has x "BY")) + (assert (not (has x "AZ"))) + (setv x (rt #[[(+ "B" "Y") (setv x (+ "A" "Z"))]])) + (assert (not (has x "BY"))) + (assert (not (has x "AZ")))) + +(defn test-multi-setv [rt] + ; https://github.com/hylang/hy/issues/1255 + (assert (re.match + r"=>\s+2\s+=>" + (rt (.replace + "(do + (setv it 0 it (+ it 1) it (+ it 1)) + it)" + "\n" " "))))) + +(defn test-error-underline-alignment [rt] + (setv err (rt "(defmacro mabcdefghi [x] x)\n(mabcdefghi)" 'err)) + (setv msg-idx (.rindex err " (mabcdefghi)")) + (setv [_ e1 e2 e3 #* _] (.splitlines (cut err msg_idx None))) + (assert (.startswith e1 " ^----------^")) + (assert (.startswith e2 "expanding macro mabcdefghi")) + (assert (or + ; PyPy can use a function's `__name__` instead of + ; `__code__.co_name`. + (.startswith e3 " TypeError: mabcdefghi") + (.startswith e3 " TypeError: (mabcdefghi)")))) + +(defn test-except-do [rt] + ; https://github.com/hylang/hy/issues/533 + (assert (has + (rt #[[(try (/ 1 0) (except [ZeroDivisionError] "hello"))]]) + "hello")) + (setv x (rt + #[[(try (/ 1 0) (except [ZeroDivisionError] "aaa" "bbb" "ccc"))]])) + (assert (not (has x "aaa"))) + (assert (not (has x "bbb"))) + (assert (has x "ccc")) + (setv x (rt + #[[(when True "xxx" "yyy" "zzz")]])) + (assert (not (has x "xxx"))) + (assert (not (has x "yyy"))) + (assert (has x "zzz"))) + +(defn test-unlocatable-hytypeerror [rt] + ; https://github.com/hylang/hy/issues/1412 + ; The chief test of interest here is that the REPL isn't itself + ; throwing an error. + (assert (has + (rt :to-return 'err #[[ + (import hy.errors) + (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))]]) + "AZ"))) + +(defn test-syntax-errors [rt] + ; https://github.com/hylang/hy/issues/2004 + (assert (has + (rt "(defn foo [/])\n(defn bar [a a])" 'err) + "SyntaxError: duplicate argument")) + ; https://github.com/hylang/hy/issues/2014 + (setv err (rt "(defn foo []\n(import re *))" 'err)) + (assert (has err "SyntaxError: import * only allowed")) + (assert (not (has err "PrematureEndOfInput")))) + +(defn test-bad-repr [rt] + ; https://github.com/hylang/hy/issues/1389 + (setv x (rt :to-return 'both #[[ + (defclass BadRepr [] (defn __repr__ [self] (/ 0))) + (BadRepr) + (+ "A" "Z")]])) + (assert (has x.err "ZeroDivisionError")) + (assert (has x.out "AZ"))) + +(defn test-py-repr [rt] + (assert (has + (rt "(+ [1] [2])") + "[1 2]")) + (assert (has + (rt "(+ [1] [2])" :py-repr True) + "[1, 2]")) + (setv x + (rt "(+ [1] [2])" :py-repr True :spy True)) + (assert (has x "[1] + [2]")) + (assert (has x "[1, 2]")) + ; --spy should work even when an exception is thrown + (assert (has + (rt "(+ [1] [2] (foof))" :py-repr True :spy True) + "[1] + [2]"))) + +(defn test-builtins [rt] + (assert (has + (rt "quit") + "Use (quit) or Ctrl-D (i.e. EOF) to exit")) + (assert (has + (rt "exit") + "Use (exit) or Ctrl-D (i.e. EOF) to exit")) + (assert (has + (rt "help") + "Use (help) for interactive help, or (help object) for help about object.")) + ; The old values of these objects come back after the REPL ends. + (assert (.startswith + (str quit) + "Use quit() or"))) + +(defn test-preserve-ps1 [rt] + ; https://github.com/hylang/hy/issues/1323#issuecomment-1310837340 + (setv sys.ps1 "chippy") + (assert (= sys.ps1 "chippy")) + (rt "(+ 1 1)") + (assert (= sys.ps1 "chippy"))) + +(defn test-input-1char [rt] + ; https://github.com/hylang/hy/issues/2430 + (assert (= + (rt "1\n") + "=> 1\n=> "))) + +(defn test-no-shebangs-allowed [rt] + (assert (has + (rt "#!/usr/bin/env hy\n" 'err) + "hy.reader.exceptions.LexException"))) diff --git a/tests/native_tests/reserved.hy b/tests/native_tests/reserved.hy deleted file mode 100644 index 3243b7e39..000000000 --- a/tests/native_tests/reserved.hy +++ /dev/null @@ -1,18 +0,0 @@ -(import hy.reserved [macros names]) - -(defn test-reserved-macros [] - (assert (is (type (macros)) frozenset)) - (assert (in "and" (macros))) - (assert (not-in "False" (macros))) - (assert (not-in "pass" (macros)))) - -(defn test-reserved-names [] - (assert (is (type (names)) frozenset)) - (assert (in "and" (names))) - (assert (in "False" (names))) - (assert (in "pass" (names))) - (assert (in "class" (names))) - (assert (in "defclass" (names))) - (assert (in "defmacro" (names))) - (assert (not-in "foo" (names))) - (assert (not-in "hy" (names)))) diff --git a/tests/native_tests/setv.hy b/tests/native_tests/setv.hy new file mode 100644 index 000000000..467868ab1 --- /dev/null +++ b/tests/native_tests/setv.hy @@ -0,0 +1,91 @@ +(import + pytest) + + +(defn test-setv [] + (setv x 1) + (setv y 1) + (assert (= x y)) + (setv y 12) + (setv x y) + (assert (= x 12)) + (assert (= y 12)) + (setv y (fn [x] 9)) + (setv x y) + (assert (= (x y) 9)) + (assert (= (y x) 9)) + (try (do (setv a.b 1) (assert False)) + (except [e [NameError]] (assert (in "name 'a' is not defined" (str e))))) + (try (do (setv b.a (fn [x] x)) (assert False)) + (except [e [NameError]] (assert (in "name 'b' is not defined" (str e))))) + (import itertools) + (setv foopermutations (fn [x] (itertools.permutations x))) + (setv p (set [#(1 3 2) #(3 2 1) #(2 1 3) #(3 1 2) #(1 2 3) #(2 3 1)])) + (assert (= (set (itertools.permutations [1 2 3])) p)) + (assert (= (set (foopermutations [3 1 2])) p)) + (setv permutations- itertools.permutations) + (setv itertools.permutations (fn [x] 9)) + (assert (= (itertools.permutations p) 9)) + (assert (= (foopermutations foopermutations) 9)) + (setv itertools.permutations permutations-) + (assert (= (set (itertools.permutations [2 1 3])) p)) + (assert (= (set (foopermutations [2 3 1])) p))) + + +(defn test-setv-pairs [] + (setv a 1 b 2) + (assert (= a 1)) + (assert (= b 2)) + (setv y 0 x 1 y x) + (assert (= y 1)) + (with [(pytest.raises hy.errors.HyLanguageError)] + (hy.eval '(setv a 1 b)))) + + +(defn test-setv-returns-none [] + + (defn an [x] + (assert (is x None))) + + (an (setv)) + (an (setv x 1)) + (assert (= x 1)) + (an (setv x 2)) + (assert (= x 2)) + (an (setv y 2 z 3)) + (assert (= y 2)) + (assert (= z 3)) + (an (setv [y z] [7 8])) + (assert (= y 7)) + (assert (= z 8)) + (an (setv #(y z) [9 10])) + (assert (= y 9)) + (assert (= z 10)) + + (setv p 11) + (setv p (setv q 12)) + (assert (= q 12)) + (an p) + + (an (setv x (defn phooey [] (setv p 1) (+ p 6)))) + (an (setv x (defclass C))) + (an (setv x (for [i (range 3)] i (+ i 1)))) + (an (setv x (assert True))) + + (an (setv x (with [(open "tests/resources/text.txt" "r")] 3))) + (assert (= x 3)) + (an (setv x (try (/ 1 2) (except [ZeroDivisionError] "E1")))) + (assert (= x .5)) + (an (setv x (try (/ 1 0) (except [ZeroDivisionError] "E2")))) + (assert (= x "E2")) + + ; https://github.com/hylang/hy/issues/1052 + (an (setv (get {} "x") 42)) + (setv l []) + (defclass Foo [object] + (defn __setattr__ [self attr val] + (.append l [attr val]))) + (setv x (Foo)) + (an (setv x.eggs "ham")) + (assert (not (hasattr x "eggs"))) + (assert (= l [["eggs" "ham"]]))) diff --git a/tests/native_tests/py3_8_only_tests.hy b/tests/native_tests/setx.hy similarity index 90% rename from tests/native_tests/py3_8_only_tests.hy rename to tests/native_tests/setx.hy index 9aff621c5..2b46ff451 100644 --- a/tests/native_tests/py3_8_only_tests.hy +++ b/tests/native_tests/setx.hy @@ -1,6 +1,3 @@ -;; Tests where the emitted code relies on Python ≥3.8. -;; conftest.py skips this file when running on Python <3.8. - (import pytest) (defn test-setx [] diff --git a/tests/native_tests/strings.hy b/tests/native_tests/strings.hy new file mode 100644 index 000000000..d0f580e08 --- /dev/null +++ b/tests/native_tests/strings.hy @@ -0,0 +1,172 @@ +;; String literals (including bracket strings and docstrings), +;; plus f-strings + +(import + re + pytest) + + +(defn test-encoding-nightmares [] + (assert (= (len "ℵℵℵ♥♥♥\t♥♥\r\n") 11))) + + +(defn test-quote-bracket-string-delim [] + (assert (= (. '#[my delim[hello world]my delim] brackets) "my delim")) + (assert (= (. '#[[squid]] brackets) "")) + (assert (is (. '"squid" brackets) None))) + + +(defn test-docstrings [] + (defn f [] "docstring" 5) + (assert (= (. f __doc__) "docstring")) + + ; a single string is the return value, not a docstring + ; (https://github.com/hylang/hy/issues/1402) + (defn f3 [] "not a docstring") + (assert (is (. f3 __doc__) None)) + (assert (= (f3) "not a docstring"))) + + +(defn test-module-docstring [] + (import tests.resources.module-docstring-example :as m) + (assert (= m.__doc__ "This is the module docstring.")) + (assert (= m.foo 5))) + + +(defn test-format-strings [] + (assert (= f"hello world" "hello world")) + (assert (= f"hello {(+ 1 1)} world" "hello 2 world")) + (assert (= f"a{ (.upper (+ "g" "k")) }z" "aGKz")) + (assert (= f"a{1}{2}b" "a12b")) + + ; Referring to a variable + (setv p "xyzzy") + (assert (= f"h{p}j" "hxyzzyj")) + + ; Including a statement and setting a variable + (assert (= f"a{(do (setv floop 4) (* floop 2))}z" "a8z")) + (assert (= floop 4)) + + ; Comments + (assert (= f"a{(+ 1 + 2 ; This is a comment. + 3)}z" "a6z")) + + ; Newlines in replacement fields + (assert (= f"ey {"bee +cee"} dee" "ey bee\ncee dee")) + + ; Conversion characters and format specifiers + (setv p:9 "other") + (setv !r "bar") + (assert (= f"a{p !r}" "a'xyzzy'")) + (assert (= f"a{p :9}" "axyzzy ")) + (assert (= f"a{p:9}" "aother")) + (assert (= f"a{p !r :9}" "a'xyzzy' ")) + (assert (= f"a{p !r:9}" "a'xyzzy' ")) + (assert (= f"a{p:9 :9}" "aother ")) + (assert (= f"a{!r}" "abar")) + (assert (= f"a{!r !r}" "a'bar'")) + + ; Fun with `r` + (assert (= f"hello {r"\n"}" r"hello \n")) + (assert (= f"hello {"\n"}" "hello \n")) + + ; Braces escaped via doubling + (assert (= f"ab{{cde" "ab{cde")) + (assert (= f"ab{{cde}}}}fg{{{{{{" "ab{cde}}fg{{{")) + (assert (= f"ab{{{(+ 1 1)}}}" "ab{2}")) + + ; Nested replacement fields + (assert (= f"{2 :{(+ 2 2)}}" " 2")) + (setv value 12.34 width 10 precision 4) + (assert (= f"result: {value :{width}.{precision}}" "result: 12.34")) + + ; Nested replacement fields with ! and : + (defclass C [object] + (defn __format__ [self format-spec] + (+ "C[" format-spec "]"))) + (assert (= f"{(C) : {(str (+ 1 1)) !r :x<5}}" "C[ '2'xx]")) + + ; \N sequences + ; https://github.com/hylang/hy/issues/2321 + (setv ampersand "wich") + (assert (= f"sand{ampersand} \N{ampersand} chips" "sandwich & chips")) + + ; Format bracket strings + (assert (= #[f[a{p !r :9}]f] "a'xyzzy' ")) + (assert (= #[f-string[result: {value :{width}.{precision}}]f-string] + "result: 12.34")) + ; https://github.com/hylang/hy/issues/2419 + (assert (= + #[f[{{escaped braces}} \n {"not escaped"}]f] + "{escaped braces} \\n not escaped")) + ; https://github.com/hylang/hy/issues/2474 + (assert (= #[f["{0}"]f] "\"0\"")) + + ; Quoting shouldn't evaluate the f-string immediately + ; https://github.com/hylang/hy/issues/1844 + (setv quoted 'f"hello {world}") + (assert (isinstance quoted hy.models.FString)) + (with [(pytest.raises NameError)] + (hy.eval quoted)) + (setv world "goodbye") + (assert (= (hy.eval quoted) "hello goodbye")) + + ;; '=' debugging syntax. + (setv foo "bar") + (assert (= f"{foo =}" "foo ='bar'")) + + ;; Whitespace is preserved. + (assert (= f"xyz{ foo = }" "xyz foo = 'bar'")) + + ;; Explicit conversion is applied. + (assert (= f"{ foo = !s}" " foo = bar")) + + ;; Format spec supercedes implicit conversion. + (setv pi 3.141593 fill "_") + (assert (= f"{pi = :{fill}^8.2f}" "pi = __3.14__")) + + ;; Format spec doesn't clobber the explicit conversion. + (with [(pytest.raises + ValueError + :match r"Unknown format code '?f'? for object of type 'str'")] + f"{pi =!s:.3f}") + + ;; Nested "=" is parsed, but fails at runtime, like Python. + (setv width 7) + (with [(pytest.raises + ValueError + :match r"I|invalid format spec(?:ifier)?")] + f"{pi =:{fill =}^{width =}.2f}")) + + +(defn test-format-string-repr-roundtrip [] + (for [orig [ + 'f"hello {(+ 1 1)} world" + 'f"a{p !r:9}" + 'f"{ foo = !s}"]] + (setv new (eval (repr orig))) + (assert (= (len new) (len orig))) + (for [[n o] (zip new orig)] + (when (hasattr o "conversion") + (assert (= n.conversion o.conversion))) + (assert (= n o))))) + + +(defn test-repr-with-brackets [] + (assert (= (repr '"foo") "hy.models.String('foo')")) + (assert (= (repr '#[[foo]]) "hy.models.String('foo', brackets='')")) + (assert (= (repr '#[xx[foo]xx]) "hy.models.String('foo', brackets='xx')")) + (assert (= (repr '#[xx[]xx]) "hy.models.String('', brackets='xx')")) + + (for [g [repr str]] + (defn f [x] (re.sub r"\n\s+" "" (g x) :count 1)) + (assert (= (f 'f"foo") + "hy.models.FString([hy.models.String('foo')])")) + (assert (= (f '#[f[foo]f]) + "hy.models.FString([hy.models.String('foo')], brackets='f')")) + (assert (= (f '#[f-x[foo]f-x]) + "hy.models.FString([hy.models.String('foo')], brackets='f-x')")) + (assert (= (f '#[f-x[]f-x]) + "hy.models.FString(brackets='f-x')")))) diff --git a/tests/native_tests/sub_py3_7_only.hy b/tests/native_tests/sub_py3_7_only.hy deleted file mode 100644 index 4ea19b839..000000000 --- a/tests/native_tests/sub_py3_7_only.hy +++ /dev/null @@ -1,9 +0,0 @@ -;; Tests where the emitted code relies on Python ≤3.7. -;; conftest.py skips this file when running on Python ≥3.8. - -(import pytest) - -(defn test-setx [] - (with [e (pytest.raises hy.errors.HySyntaxError)] - (hy.eval '(setx x 1))) - (assert (= "setx requires Python 3.8 or later"))) diff --git a/tests/native_tests/try.hy b/tests/native_tests/try.hy new file mode 100644 index 000000000..54ba5bd00 --- /dev/null +++ b/tests/native_tests/try.hy @@ -0,0 +1,203 @@ +;; Tests of `try` and `raise` + +(import + pytest) + + +(defn test-try-missing-parts [] + (assert (is (try) None)) + (assert (= (try 1) 1)) + (assert (is (try (except [])) None)) + (assert (is (try (finally)) None)) + (assert (= (try 1 (finally 2)) 1)) + (assert (is (try (else)) None)) + (assert (= (try 1 (else 2)) 2))) + + +(defn test-try-multiple-statements [] + (setv value 0) + (try (+= value 1) (+= value 2) (except [IOError]) (except [])) + (assert (= value 3))) + + +(defn test-try-multiple-expressions [] + ; https://github.com/hylang/hy/issues/1584 + + (setv l []) + (defn f [] (.append l 1)) + (try (f) (f) (f) (except [IOError])) + (assert (= l [1 1 1])) + (setv l []) + (try (f) (f) (f) (except [IOError]) (else (f))) + (assert (= l [1 1 1 1]))) + + +(defn test-raise-nullary [] + + ;; Test correct (raise) + (setv passed False) + (try + (try + (do) + (raise IndexError) + (except [IndexError] (raise))) + (except [IndexError] + (setv passed True))) + (assert passed) + + ;; Test incorrect (raise) + (setv passed False) + (try + (raise) + (except [RuntimeError] + (setv passed True))) + (assert passed)) + + +(defn test-try-clauses [] + + (defmacro try-it [body v1 v2] + `(assert (= (_try-it (fn [] ~body)) [~v1 ~v2]))) + (defn _try-it [callback] + (setv did-finally-clause? False) + (try + (callback) + (except [ZeroDivisionError] + (setv out ["aaa" None])) + (except [[IndexError NameError]] + (setv out ["bbb" None])) + (except [e TypeError] + (setv out ["ccc" (type e)])) + (except [e [KeyError AttributeError]] + (setv out ["ddd" (type e)])) + (except [] + (setv out ["eee" None])) + (else + (setv out ["zzz" None])) + (finally + (setv did-finally-clause? True))) + (assert did-finally-clause?) + out) + + (try-it (/ 1 0) "aaa" None) + (try-it (get "foo" 5) "bbb" None) + (try-it unbound "bbb" None) + (try-it (abs "hi") "ccc" TypeError) + (try-it (get {1 2} 3) "ddd" KeyError) + (try-it True.a "ddd" AttributeError) + (try-it (raise ValueError) "eee" None) + (try-it "hi" "zzz" None)) + + +(defn test-finally-executes-for-uncaught-exception [] + (setv x "") + (with [(pytest.raises ZeroDivisionError)] + (try + (+= x "a") + (/ 1 0) + (+= x "b") + (finally + (+= x "c")))) + (assert (= x "ac"))) + + +(defn test-nonsyntactical-except [] + #[[Test that [except ...] and ("except" ...) aren't treated like (except ...), + and that the code there is evaluated normally.]] + + (setv x 0) + (try + (+= x 1) + ("except" [IOError] (+= x 1)) + (except [])) + + (assert (= x 2)) + + (setv x 0) + (try + (+= x 1) + [except [IOError] (+= x 1)] + (except [])) + + (assert (= x 2))) + + +(defn test-try-except-return [] + "Ensure we can return from an `except` form." + (assert (= ((fn [] (try xxx (except [NameError] (+ 1 1))))) 2)) + (setv foo (try xxx (except [NameError] (+ 1 1)))) + (assert (= foo 2)) + (setv foo (try (+ 2 2) (except [NameError] (+ 1 1)))) + (assert (= foo 4))) + + +(defn test-try-else-return [] + "Ensure we can return from the `else` clause of a `try`." + ; https://github.com/hylang/hy/issues/798 + + (assert (= "ef" ((fn [] + (try + (+ "a" "b") + (except [NameError] + (+ "c" "d")) + (else + (+ "e" "f"))))))) + + (setv foo + (try + (+ "A" "B") + (except [NameError] + (+ "C" "D")) + (else + (+ "E" "F")))) + (assert (= foo "EF")) + + ; Check that the lvalue isn't assigned by the main `try` body + ; when there's an `else`. + (setv x 1) + (setv y 0) + (setv x + (try + (+ "G" "H") + (except [NameError] + (+ "I" "J")) + (else + (setv y 1) + (assert (= x 1)) + ; `x` still has its value from before the `try`. + (+ "K" "L")))) + (assert (= x "KL")) + (assert (= y 1))) + + +(do-mac (when hy._compat.PY3_11 '(defn test-except* [] + (setv got "") + + (setv return-value (try + (raise (ExceptionGroup "meep" [(KeyError) (ValueError)])) + (except* [KeyError] + (+= got "k") + "r1") + (except* [IndexError] + (+= got "i") + "r2") + (except* [ValueError] + (+= got "v") + "r3") + (else + (+= got "e") + "r4") + (finally + (+= got "f") + "r5"))) + + (assert (= got "kvf")) + (assert (= return-value "r3"))))) + + +(defn test-raise-from [] + (assert (is NameError (type (. + (try + (raise ValueError :from NameError) + (except [e [ValueError]] e)) + __cause__))))) diff --git a/tests/native_tests/unpack.hy b/tests/native_tests/unpack.hy new file mode 100644 index 000000000..181d7ecd6 --- /dev/null +++ b/tests/native_tests/unpack.hy @@ -0,0 +1,40 @@ +(defn test-star-unpacking [] + (setv l [1 2 3]) + (setv d {"a" "x" "b" "y"}) + (defn fun [[x1 None] [x2 None] [x3 None] [x4 None] [a None] [b None] [c None]] + [x1 x2 x3 x4 a b c]) + (assert (= (fun 5 #* l) [5 1 2 3 None None None])) + (assert (= (+ #* l) 6)) + (assert (= (fun 5 #** d) [5 None None None "x" "y" None])) + (assert (= (fun 5 #* l #** d) [5 1 2 3 "x" "y" None]))) + + +(defn test-extended-unpacking-1star-lvalues [] + (setv [x #* y] [1 2 3 4]) + (assert (= x 1)) + (assert (= y [2 3 4])) + (setv [a #* b c] "ghijklmno") + (assert (= a "g")) + (assert (= b (list "hijklmn"))) + (assert (= c "o"))) + + +(defn test-unpacking-pep448-1star [] + (setv l [1 2 3]) + (setv p [4 5]) + (assert (= ["a" #* l "b" #* p #* l] ["a" 1 2 3 "b" 4 5 1 2 3])) + (assert (= #("a" #* l "b" #* p #* l) #("a" 1 2 3 "b" 4 5 1 2 3))) + (assert (= #{"a" #* l "b" #* p #* l} #{"a" "b" 1 2 3 4 5})) + (defn f [#* args] args) + (assert (= (f "a" #* l "b" #* p #* l) #("a" 1 2 3 "b" 4 5 1 2 3))) + (assert (= (+ #* l #* p) 15)) + (assert (= (and #* l) 3))) + + +(defn test-unpacking-pep448-2star [] + (setv d1 {"a" 1 "b" 2}) + (setv d2 {"c" 3 "d" 4}) + (assert (= {1 "x" #** d1 #** d2 2 "y"} {"a" 1 "b" 2 "c" 3 "d" 4 1 "x" 2 "y"})) + (defn fun [[a None] [b None] [c None] [d None] [e None] [f None]] + [a b c d e f]) + (assert (= (fun #** d1 :e "eee" #** d2) [1 2 3 4 "eee" None]))) diff --git a/tests/native_tests/when.hy b/tests/native_tests/when.hy deleted file mode 100644 index 3fb5b51b7..000000000 --- a/tests/native_tests/when.hy +++ /dev/null @@ -1,10 +0,0 @@ -(defn test-when [] - (assert (= (when True 1) 1)) - (assert (= (when True 1 2) 2)) - (assert (= (when True 1 3) 3)) - (assert (= (when False 2) None)) - (assert (= (when (= 1 2) 42) None)) - (assert (= (when (= 2 2) 42) 42)) - - (assert (is (when (do (setv x 3) True)) None)) - (assert (= x 3))) diff --git a/tests/native_tests/with_test.hy b/tests/native_tests/with.hy similarity index 56% rename from tests/native_tests/with_test.hy rename to tests/native_tests/with.hy index 7d3cfb994..fb2b364fb 100644 --- a/tests/native_tests/with_test.hy +++ b/tests/native_tests/with.hy @@ -1,4 +1,16 @@ -(import pytest) +(import + asyncio + pytest + tests.resources [async-test AsyncWithTest]) + +(defn test-context [] + (with [fd (open "tests/resources/text.txt" "r")] (assert fd)) + (with [(open "tests/resources/text.txt" "r")] (do))) + +(defn test-with-return [] + (defn read-file [filename] + (with [fd (open filename "r" :encoding "UTF-8")] (.read fd))) + (assert (= (read-file "tests/resources/text.txt") "TAARGÜS TAARGÜS\n"))) (defclass WithTest [object] (defn __init__ [self val] @@ -38,6 +50,41 @@ (assert (= t2 2)) (assert (= t3 3)))) +(defn [async-test] test-single-with/a [] + (asyncio.run + ((fn/a [] + (with/a [t (AsyncWithTest 1)] + (assert (= t 1))))))) + +(defn [async-test] test-two-with/a [] + (asyncio.run + ((fn/a [] + (with/a [t1 (AsyncWithTest 1) + t2 (AsyncWithTest 2)] + (assert (= t1 1)) + (assert (= t2 2))))))) + +(defn [async-test] test-thrice-with/a [] + (asyncio.run + ((fn/a [] + (with/a [t1 (AsyncWithTest 1) + t2 (AsyncWithTest 2) + t3 (AsyncWithTest 3)] + (assert (= t1 1)) + (assert (= t2 2)) + (assert (= t3 3))))))) + +(defn [async-test] test-quince-with/a [] + (asyncio.run + ((fn/a [] + (with/a [t1 (AsyncWithTest 1) + t2 (AsyncWithTest 2) + t3 (AsyncWithTest 3) + _ (AsyncWithTest 4)] + (assert (= t1 1)) + (assert (= t2 2)) + (assert (= t3 3))))))) + (defn test-unnamed-context-with [] "`_` get compiled to unnamed context" (with [_ (WithTest 1) diff --git a/tests/resources/__init__.py b/tests/resources/__init__.py index fe698d49a..458d85fd9 100644 --- a/tests/resources/__init__.py +++ b/tests/resources/__init__.py @@ -1,3 +1,10 @@ +import pytest + +from hy._compat import PYODIDE + +in_init = "chippy" + + def kwtest(*args, **kwargs): return kwargs @@ -6,6 +13,12 @@ def function_with_a_dash(): pass +can_test_async = not PYODIDE +async_test = pytest.mark.skipif( + not can_test_async, reason="`asyncio.run` not implemented" +) + + class AsyncWithTest: def __init__(self, val): self.val = val diff --git a/tests/resources/hy_repr_str_tests.txt b/tests/resources/hy_repr_str_tests.txt index 730849526..f61568966 100644 --- a/tests/resources/hy_repr_str_tests.txt +++ b/tests/resources/hy_repr_str_tests.txt @@ -1,53 +1,69 @@ ;; One test case per line. +;; If the case doesn't begin with a single quote or a colon, it also +;; gets a variant case that does. ;; Lines starting with `;` are comments. -;; Lines starting with `!` specify two tests: one with a leading -;; single quote and one without. ;; * Numeric -!None -!False -!True +None +False +True -!5 -!5.1 +5 +5.1 -!Inf -!-Inf -!NaN +Inf +-Inf +NaN -!5j -!5.1j -!2+1j -!1.2+3.4j -!Inf-Infj -!NaN+NaNj +5j +5.1j +2+1j +1.2+3.4j +Inf-Infj +NaN+NaNj -!(Fraction 1 3) +(Fraction 1 3) ;; * Symbols and keywords 'mysymbol 'my♥symbol? +'. +'.. +'... +'.... :mykeyword :my♥keyword? : +'':mykeyword +; A keyword with only one single quote gets represented without the +; quote (which is equivalent), so it's tested in +; `test-hy-repr-roundtrip-from-value`. + +;; * Dotted identifiers + +'foo.bar +'.foo.bar +'..foo.bar +'...foo.bar +'....foo.bar ;; * Stringy thingies -!"" -!b"" -!(bytearray b"") +"" +b"" +(bytearray b"") -!"apple bloom" -!b"apple bloom" -!(bytearray b"apple bloom") -!"⚘" +"apple bloom" +b"apple bloom" +(bytearray b"apple bloom") +"⚘" -!"single ' quotes" -!b"single ' quotes" -!"\"double \" quotes\"" -!b"\"double \" quotes\"" +"single ' quotes" +b"single ' quotes" +"\"double \" quotes\"" +b"\"double \" quotes\"" '#[[bracketed string]] '#[delim[bracketed string]delim] @@ -55,38 +71,48 @@ ;; * Collections -![] -!#() -!#{} -!(frozenset #{}) -!{} +[] +#() +#{} +(frozenset #{}) +{} -!['[]] -![1 2 3] -!#(1 2 3) -!#{1 2 3} +['[]] +[1 2 3] +#(1 2 3) +#{1 2 3} '#{3 2 1 2} -!(frozenset #{1 2 3}) -!{"a" 1 "b" 2} -!{"b" 2 "a" 1} +(frozenset #{1 2 3}) +{"a" 1 "b" 2} +{"b" 2 "a" 1} '[1 a 3] -![1 'a 3] -![1 '[2 3] 4] +[1 'a 3] +[1 '[2 3] 4] -!(deque []) -!(deque [1 2.5 None "hello"]) -!(ChainMap {}) -!(ChainMap {1 2} {3 4}) -!(OrderedDict []) -!(OrderedDict [#(1 2) #(3 4)]) +(deque []) +(deque [1 2.5 None "hello"]) +(ChainMap {}) +(ChainMap {1 2} {3 4}) +(OrderedDict []) +(OrderedDict [#(1 2) #(3 4)]) ;; * Expressions '(+ 1 2) '(f a b) '(f #* args #** kwargs) -![1 [2 3] #(4 #('mysymbol :mykeyword)) {"a" b"hello"} '(f #* a #** b)] +'(.real 3) +'(.a.b.c foo) +'(math.sqrt 25) +'(. a) +'(. None) +'(. 1 2) +; `(. a b)` will render as `a.b`. +'(.. 1 2) +'(.. a b) +[1 [2 3] #(4 #('mysymbol :mykeyword)) {"a" b"hello"} '(f #* a #** b)] +'(quote) ;; * Quasiquoting @@ -94,6 +120,10 @@ '[1 `[~foo ~@bar] 4] '[1 `[~(+ 1 2) ~@(+ [1] [2])] 4] '[1 `[~(do (print x 'y) 1)] 4] +'(quasiquote 1 2 3) +'[1 `[2 (unquote foo bar)]] +'[a ~@b] +'[a ~ @b] ;; * F-strings @@ -107,23 +137,23 @@ ;; * Ranges and slices -!(range 5) -!(slice 5) -!(range 2 5) -!(slice 2 5) -!(range 5 2) -!(slice 5 2) -!(range 0 5 2) -!(slice 0 5 2) -!(range 0 5 -2) -!(slice 0 5 -2) -!(slice [1 2 3] #(4 6) "hello") +(range 5) +(slice 5) +(range 2 5) +(slice 2 5) +(range 5 2) +(slice 5 2) +(range 0 5 2) +(slice 0 5 2) +(range 0 5 -2) +(slice 0 5 -2) +(slice [1 2 3] #(4 6) "hello") ;; * Regexen -!(re.compile "foo") -!(re.compile "\\Ax\\Z") -!(re.compile "'") -!(re.compile "\"") -!(re.compile "foo" re.IGNORECASE) -!(re.compile "foo" (| re.IGNORECASE re.MULTILINE)) +(re.compile "foo") +(re.compile "\\Ax\\Z") +(re.compile "'") +(re.compile "\"") +(re.compile "foo" re.IGNORECASE) +(re.compile "foo" (| re.IGNORECASE re.MULTILINE)) diff --git a/tests/resources/importer/circular.hy b/tests/resources/importer/circular.hy index 4825b5f29..c5120a67c 100644 --- a/tests/resources/importer/circular.hy +++ b/tests/resources/importer/circular.hy @@ -2,4 +2,3 @@ (defn f [] (import circular) circular.a) -(print (f)) diff --git a/tests/resources/importer/foo/__init__.hy b/tests/resources/importer/foo/__init__.hy index 5909d57b4..2902aa116 100644 --- a/tests/resources/importer/foo/__init__.hy +++ b/tests/resources/importer/foo/__init__.hy @@ -1,2 +1 @@ -(print "This is __init__.hy") (setv ext "hy") diff --git a/tests/resources/importer/foo/__init__.py b/tests/resources/importer/foo/__init__.py index 7e89b4607..306d46cb8 100644 --- a/tests/resources/importer/foo/__init__.py +++ b/tests/resources/importer/foo/__init__.py @@ -1,2 +1 @@ -print("This is __init__.py") ext = "py" diff --git a/tests/resources/importer/foo/some_mod.hy b/tests/resources/importer/foo/some_mod.hy index 10db45cff..2902aa116 100644 --- a/tests/resources/importer/foo/some_mod.hy +++ b/tests/resources/importer/foo/some_mod.hy @@ -1,2 +1 @@ -(print "This is test_mod.hy") (setv ext "hy") diff --git a/tests/resources/importer/foo/some_mod.py b/tests/resources/importer/foo/some_mod.py index 40c80e5c4..306d46cb8 100644 --- a/tests/resources/importer/foo/some_mod.py +++ b/tests/resources/importer/foo/some_mod.py @@ -1,2 +1 @@ -print("This is test_mod.py") ext = "py" diff --git a/tests/resources/local_req_example.hy b/tests/resources/local_req_example.hy new file mode 100644 index 000000000..c9c2bc80f --- /dev/null +++ b/tests/resources/local_req_example.hy @@ -0,0 +1,8 @@ +(defmacro wiz [] + "remote wiz doc" + "remote wiz") +(defmacro get-wiz [] + (wiz)) +(defmacro helper [] + "remote helper doc" + "remote helper macro") diff --git a/tests/resources/macro_with_require.hy b/tests/resources/macro_with_require.hy index c1b69f56f..4650ec32e 100644 --- a/tests/resources/macro_with_require.hy +++ b/tests/resources/macro_with_require.hy @@ -4,11 +4,11 @@ (defmacro test-module-macro [a] "The variable `macro-level-var' here should not bind to the same-named symbol -in the expansion of `nonlocal-test-macro'." +in the expansion of `remote-test-macro'." (setv macro-level-var "tests.resources.macros.macro-with-require") - `(nonlocal-test-macro ~a)) + `(remote-test-macro ~a)) (defmacro test-module-macro-2 [a] - "The macro `local-test-macro` isn't in this module's namespace, so it better + "The macro `home-test-macro` isn't in this module's namespace, so it better be in the expansion's!" - `(local-test-macro ~a)) + `(home-test-macro ~a)) diff --git a/tests/resources/macros.hy b/tests/resources/macros.hy index 004d55a42..2b8094eae 100644 --- a/tests/resources/macros.hy +++ b/tests/resources/macros.hy @@ -6,7 +6,7 @@ (defmacro test-macro-2 [] '(setv qup 2)) -(defmacro nonlocal-test-macro [x] +(defmacro remote-test-macro [x] "When called from `macro-with-require`'s macro(s), the first instance of `module-name-var` should resolve to the value in the module where this is defined, then the expansion namespace/module" diff --git a/tests/resources/more_test_macros.hy b/tests/resources/more_test_macros.hy new file mode 100644 index 000000000..478c87bf4 --- /dev/null +++ b/tests/resources/more_test_macros.hy @@ -0,0 +1,8 @@ +(defmacro bairn [#* tree] + `[14 ~@tree]) + +(defmacro cairn [ #* tree] + `[15 ~@tree]) + +(defmacro _dairn [] + `[16 ~@tree]) diff --git a/tests/resources/pydemo.hy b/tests/resources/pydemo.hy index 0a12c7347..80c4c0901 100644 --- a/tests/resources/pydemo.hy +++ b/tests/resources/pydemo.hy @@ -51,8 +51,8 @@ Call me Ishmael. Some years ago—never mind how long precisely—having little (setv boolexpr (and (or True False) (not (and True False)))) (setv condexpr (if "" "x" "y")) (setv mylambda (fn [x] (+ x "z"))) -(setv annotated-lambda-ret (fn #^int [] 1)) -(setv annotated-lambda-params (fn [#^int a * #^str [b "hello world!"]] #(a b))) +(setv annotated-lambda-ret (fn #^ int [] 1)) +(setv annotated-lambda-params (fn [#^ int a * #^ str [b "hello world!"]] #(a b))) (setv fstring1 f"hello {(+ 1 1)} world") (setv p "xyzzy") diff --git a/tests/resources/text.txt b/tests/resources/text.txt new file mode 100644 index 000000000..fa80786a8 --- /dev/null +++ b/tests/resources/text.txt @@ -0,0 +1 @@ +TAARGÜS TAARGÜS diff --git a/tests/resources/tlib.hy b/tests/resources/tlib.hy index 2fb4d28ca..8d18233c4 100644 --- a/tests/resources/tlib.hy +++ b/tests/resources/tlib.hy @@ -9,7 +9,7 @@ (defmacro ✈ [arg] `(+ "plane " ~arg)) -(defreader upper +(defreader upper! (let [node (&reader.parse-one-form)] (if (isinstance node #(hy.models.Symbol hy.models.String)) (.__class__ node (.upper node)) diff --git a/tests/resources/tp.hy b/tests/resources/tp.hy new file mode 100644 index 000000000..e02a47299 --- /dev/null +++ b/tests/resources/tp.hy @@ -0,0 +1,10 @@ +"Helpers for testing type parameters." + +(import typing [TypeVar TypeVarTuple ParamSpec TypeAliasType]) + +(defn show [f] + (lfor + tp f.__type_params__ + [(type tp) tp.__name__ + (getattr tp "__bound__" None) + (getattr tp "__constraints__" #())])) diff --git a/tests/test_bin.py b/tests/test_bin.py index 45bae911a..364c2cf27 100644 --- a/tests/test_bin.py +++ b/tests/test_bin.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# fmt: off import builtins import os @@ -12,7 +11,12 @@ import pytest -from hy._compat import PY3_9 +from hy._compat import PY3_9, PYODIDE + +if PYODIDE: + pytest.skip( + '`subprocess.Popen` not implemented on Pyodide', + allow_module_level = True) def pyr(s=""): @@ -21,25 +25,29 @@ def pyr(s=""): def run_cmd( cmd, stdin_data=None, expect=0, dontwritebytecode=False, - stdout=subprocess.PIPE): + cwd=None, stdout=subprocess.PIPE): env = dict(os.environ) if dontwritebytecode: env["PYTHONDONTWRITEBYTECODE"] = "1" else: env.pop("PYTHONDONTWRITEBYTECODE", None) - p = subprocess.Popen( - shlex.split(cmd), - stdin=subprocess.PIPE, + # ensure hy root dir is in Python's path, + # so that we can import/require modules within tests/ + env["PYTHONPATH"] = str(Path().resolve()) + os.pathsep + env.get("PYTHONPATH", "") + + result = subprocess.run( + shlex.split(cmd) if isinstance(cmd, str) else cmd, + input=stdin_data, stdout=stdout, stderr=subprocess.PIPE, universal_newlines=True, shell=False, env=env, + cwd=cwd, ) - output = p.communicate(input=stdin_data) - assert p.wait() == expect - return output + assert result.returncode == expect + return (result.stdout, result.stderr) def rm(fpath): try: @@ -56,114 +64,20 @@ def test_simple(): def test_stdin(): - output, _ = run_cmd("hy", '(.upper "hello")') - assert "HELLO" in output - - output, _ = run_cmd("hy --spy", '(.upper "hello")') - assert ".upper()" in output - assert "HELLO" in output - - # --spy should work even when an exception is thrown - output, _ = run_cmd("hy --spy", "(foof)") - assert "foof()" in output - - -def test_stdin_multiline(): - output, _ = run_cmd("hy", '(+ "a" "b"\n"c" "d")') - assert '"abcd"' in output - - -def test_history(): - output, _ = run_cmd("hy", '''(+ "a" "b") - (+ "c" "d") - (+ "e" "f") - (.format "*1: {}, *2: {}, *3: {}," *1 *2 *3)''') - assert '"*1: ef, *2: cd, *3: ab,"' in output - - output, _ = run_cmd("hy", '''(raise (Exception "TEST ERROR")) - (+ "err: " (str *e))''') - assert '"err: TEST ERROR"' in output - - -def test_stdin_comments(): - _, err_empty = run_cmd("hy", "") - - output, err = run_cmd("hy", '(+ "a" "b") ; "c"') - assert '"ab"' in output - assert err == err_empty - - _, err = run_cmd("hy", "; 1") - assert err == err_empty - - -def test_stdin_assignment(): - # If the last form is an assignment, don't print the value. - - output, _ = run_cmd("hy", '(setv x (+ "A" "Z"))') - assert "AZ" not in output - - output, _ = run_cmd("hy", '(setv x (+ "A" "Z")) (+ "B" "Y")') - assert "AZ" not in output - assert "BY" in output - - output, _ = run_cmd("hy", '(+ "B" "Y") (setv x (+ "A" "Z"))') - assert "AZ" not in output - assert "BY" not in output - - -def test_multi_setv(): - # https://github.com/hylang/hy/issues/1255 - output, _ = run_cmd("hy", """(do - (setv it 0 it (+ it 1) it (+ it 1)) - it)""".replace("\n", " ")) - assert re.match(r"=>\s+2\s+=>", output) - + # https://github.com/hylang/hy/issues/2438 + code = '(+ "P" "Q")\n(print (+ "R" "S"))\n(+ "T" "U")' -def test_stdin_error_underline_alignment(): - _, err = run_cmd("hy", "(defmacro mabcdefghi [x] x)\n(mabcdefghi)") + # Without `-i`, the standard input is run as a script. + out, _ = run_cmd("hy", code) + assert "PQ" not in out + assert "RS" in out + assert "TU" not in out - msg_idx = err.rindex(" (mabcdefghi)") - assert msg_idx - err_parts = err[msg_idx:].splitlines() - assert err_parts[1].startswith(" ^----------^") - assert err_parts[2].startswith("expanding macro mabcdefghi") - assert ( - err_parts[3].startswith(" TypeError: mabcdefghi") - or - # PyPy can use a function's `__name__` instead of - # `__code__.co_name`. - err_parts[3].startswith(" TypeError: (mabcdefghi)") - ) - - -def test_stdin_except_do(): - # https://github.com/hylang/hy/issues/533 - - output, _ = run_cmd("hy", - '(try (/ 1 0) (except [ZeroDivisionError] "hello"))') - assert "hello" in output - - output, _ = run_cmd("hy", - '(try (/ 1 0) (except [ZeroDivisionError] "aaa" "bbb" "ccc"))') - assert "aaa" not in output - assert "bbb" not in output - assert "ccc" in output - - output, _ = run_cmd("hy", - '(when True "xxx" "yyy" "zzz")') - assert "xxx" not in output - assert "yyy" not in output - assert "zzz" in output - - -def test_stdin_unlocatable_hytypeerror(): - # https://github.com/hylang/hy/issues/1412 - # The chief test of interest here is the returncode assertion - # inside run_cmd. - _, err = run_cmd("hy", """ - (import hy.errors) - (raise (hy.errors.HyTypeError (+ "A" "Z") None '[] None))""") - assert "AZ" in err + # With it, the standard input is fed to the REPL. + out, _ = run_cmd("hy -i", code) + assert "PQ" in out + assert "RS" in out + assert "TU" in out def test_error_parts_length(): @@ -187,7 +101,7 @@ def test_error_parts_length(): """ # Up-arrows right next to each other. - _, err = run_cmd("hy", prg_str.format(3, 3, 1, 2)) + _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 2)) msg_idx = err.rindex("HyLanguageError:") assert msg_idx @@ -207,7 +121,7 @@ def test_error_parts_length(): assert obs.startswith(exp) # Make sure only one up-arrow is printed - _, err = run_cmd("hy", prg_str.format(3, 3, 1, 1)) + _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 1)) msg_idx = err.rindex("HyLanguageError:") assert msg_idx @@ -216,8 +130,7 @@ def test_error_parts_length(): # Make sure lines are printed in between arrows separated by more than one # character. - _, err = run_cmd("hy", prg_str.format(3, 3, 1, 6)) - print(err) + _, err = run_cmd("hy -i", prg_str.format(3, 3, 1, 6)) msg_idx = err.rindex("HyLanguageError:") assert msg_idx @@ -225,43 +138,6 @@ def test_error_parts_length(): assert err_parts[2] == " ^----^" -def test_syntax_errors(): - # https://github.com/hylang/hy/issues/2004 - _, err = run_cmd("hy", "(defn foo [/])\n(defn bar [a a])") - assert "SyntaxError: duplicate argument" in err - - # https://github.com/hylang/hy/issues/2014 - _, err = run_cmd("hy", "(defn foo []\n(import re *))") - assert "SyntaxError: import * only allowed" in err - assert "PrematureEndOfInput" not in err - - -def test_stdin_bad_repr(): - # https://github.com/hylang/hy/issues/1389 - output, err = run_cmd("hy", """ - (defclass BadRepr [] (defn __repr__ [self] (/ 0))) - (BadRepr) - (+ "A" "Z")""") - assert "ZeroDivisionError" in err - assert "AZ" in output - - -def test_stdin_py_repr(): - output, _ = run_cmd("hy", "(+ [1] [2])") - assert "[1 2]" in output - - output, _ = run_cmd(pyr(), "(+ [1] [2])") - assert "[1, 2]" in output - - output, _ = run_cmd(pyr("--spy"), "(+ [1] [2])") - assert "[1]+[2]" in output.replace(" ", "") - assert "[1, 2]" in output - - # --spy should work even when an exception is thrown - output, _ = run_cmd(pyr("--spy"), "(+ [1] [2] (foof))") - assert "[1]+[2]" in output.replace(" ", "") - - def test_mangle_m(): # https://github.com/hylang/hy/issues/1445 @@ -298,6 +174,10 @@ def test_cmd(): _, err = run_cmd("""hy -c '(print (.upper "hello")'""", expect=1) assert "Premature end of input" in err + # No shebang is allowed. + _, err = run_cmd("""hy -c '#!/usr/bin/env hy'""", expect = 1) + assert "LexException" in err + # https://github.com/hylang/hy/issues/1879 output, _ = run_cmd( """hy -c '(setv x "bing") (defn f [] (+ "fiz" x)) (print (f))'""" @@ -311,8 +191,8 @@ def test_cmd(): assert "<-c|AA|ZZ|-m>" in output -def test_icmd(): - output, _ = run_cmd("""hy -i '(.upper "hello")'""", '(.upper "bye")') +def test_icmd_string(): + output, _ = run_cmd("""hy -i -c '(.upper "hello")'""", '(.upper "bye")') assert "HELLO" in output assert "BYE" in output @@ -322,11 +202,25 @@ def test_icmd_file(): assert "CUTTLEFISH" in output +def test_icmd_shebang(tmp_path): + (tmp_file := tmp_path / 'icmd_with_shebang.hy').write_text('#!/usr/bin/env hy\n(setv order "Sepiida")') + output, error = run_cmd(["hy", "-i", tmp_file], '(.upper order)') + assert "#!/usr/bin/env" not in error + assert "SEPIIDA" in output + + def test_icmd_and_spy(): - output, _ = run_cmd('hy --spy -i "(+ [] [])"', "(+ 1 1)") + output, _ = run_cmd('hy --spy -i -c "(+ [] [])"', "(+ 1 1)") assert "[] + []" in output +def test_empty_file(tmp_path): + # https://github.com/hylang/hy/issues/2427 + (tmp_path / 'foo.hy').write_text('') + run_cmd(['hy', (tmp_path / 'foo.hy')]) + # This asserts that the return code is 0. + + def test_missing_file(): _, err = run_cmd("hy foobarbaz", expect=2) assert "No such file" in err @@ -340,12 +234,20 @@ def test_file_with_args(): assert "foo" in run_cmd(f"{cmd} -i foo -c bar")[0] +def test_ifile_with_args(): + cmd = "hy -i tests/resources/argparse_ex.hy" + assert "usage" in run_cmd(f"{cmd} -h")[0] + assert "got c" in run_cmd(f"{cmd} -c bar")[0] + assert "foo" in run_cmd(f"{cmd} -i foo")[0] + assert "foo" in run_cmd(f"{cmd} -i foo -c bar")[0] + + def test_hyc(): output, _ = run_cmd("hyc -h") assert "usage" in output path = "tests/resources/argparse_ex.hy" - _, err = run_cmd("hyc " + path) + _, err = run_cmd(["hyc", path]) assert "Compiling" in err assert os.path.exists(cache_from_source(path)) rm(cache_from_source(path)) @@ -356,23 +258,6 @@ def test_hyc_missing_file(): assert "[Errno 2]" in err -def test_builtins(): - # The REPL replaces `builtins.help` etc. - - output, _ = run_cmd("hy", 'quit') - assert "Use (quit) or Ctrl-D (i.e. EOF) to exit" in output - - output, _ = run_cmd("hy", 'exit') - assert "Use (exit) or Ctrl-D (i.e. EOF) to exit" in output - - output, _ = run_cmd("hy", 'help') - assert "Use (help) for interactive help, or (help object) for help about object." in output - - # Just importing `hy.cmdline` doesn't modify these objects. - import hy.cmdline - assert "help(object)" in str(builtins.help) - - def test_no_main(): output, _ = run_cmd("hy tests/resources/bin/nomain.hy") assert "This Should Still Work" in output @@ -466,7 +351,7 @@ def testc_file_sys_path(): rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(file_relative_path)) - output, _ = run_cmd(f"{binary} {test_file}") + output, _ = run_cmd([binary, test_file]) assert repr(file_relative_path) in output @@ -494,12 +379,12 @@ def test_circular_macro_require(): test_file = "tests/resources/bin/circular_macro_require.hy" rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "WOWIE" # Now, with bytecode assert os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "WOWIE" @@ -513,45 +398,41 @@ def test_macro_require(): test_file = "tests/resources/bin/require_and_eval.hy" rm(cache_from_source(test_file)) assert not os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "abc" # Now, with bytecode assert os.path.exists(cache_from_source(test_file)) - output, _ = run_cmd("hy {}".format(test_file)) + output, _ = run_cmd(["hy", test_file]) assert output.strip() == "abc" def test_tracebacks(): """Make sure the printed tracebacks are correct.""" - # We want the filtered tracebacks. - os.environ["HY_DEBUG"] = "" - def req_err(x): - assert x == "hy.errors.HyRequireError: No module named " "'not_a_real_module'" + assert x == "hy.errors.HyRequireError: No module named 'not_a_real_module'" # Modeled after # > python -c 'import not_a_real_module' # Traceback (most recent call last): # File "", line 1, in # ImportError: No module named not_a_real_module - _, error = run_cmd("hy", "(require not-a-real-module)") + _, error = run_cmd("hy", "(require not-a-real-module)", expect=1) error_lines = error.splitlines() if error_lines[-1] == "": del error_lines[-1] assert len(error_lines) <= 10 # Rough check for the internal traceback filtering - req_err(error_lines[4]) + req_err(error_lines[-1]) _, error = run_cmd('hy -c "(require not-a-real-module)"', expect=1) error_lines = error.splitlines() assert len(error_lines) <= 4 req_err(error_lines[-1]) - output, error = run_cmd('hy -i "(require not-a-real-module)"') + output, error = run_cmd('hy -i -c "(require not-a-real-module)"', '') assert output.startswith("=> ") - print(error.splitlines()) req_err(error.splitlines()[2]) # Modeled after @@ -577,8 +458,8 @@ def req_err(x): # ^ # SyntaxError: EOL while scanning string literal # >>> - output, error = run_cmd(r'hy -i "(print \""') - assert output.startswith("=> ") + output, error = run_cmd(r'hy -c "(print \""', expect=1) + assert output == '' assert re.match(peoi_re, error) # Modeled after @@ -587,7 +468,8 @@ def req_err(x): # File "", line 1, in # NameError: name 'a' is not defined output, error = run_cmd('hy -c "(print a)"', expect=1) - error_lines = error.splitlines() + # Filter out the underline added by Python 3.11. + error_lines = [x for x in error.splitlines() if set(x) != {" ", "^"}] assert error_lines[3] == ' File "", line 1, in ' # PyPy will add "global" to this error message, so we work around that. assert error_lines[-1].strip().replace(" global", "") == ( @@ -605,31 +487,40 @@ def req_err(x): assert error_lines[-1].startswith("TypeError") +def test_traceback_shebang(tmp_path): + # https://github.com/hylang/hy/issues/2405 + (tmp_path / 'ex.hy').write_text('#!my cool shebang\n(/ 1 0)') + _, error = run_cmd(['hy', tmp_path / 'ex.hy'], expect = 1) + assert 'ZeroDivisionError' + assert 'my cool shebang' not in error + assert '(/ 1 0)' in error + + def test_hystartup(): # spy == True and custom repl-output-fn os.environ["HYSTARTUP"] = "tests/resources/hystartup.hy" - output, _ = run_cmd("hy", "[1 2]") + output, _ = run_cmd("hy -i", "[1 2]") assert "[1, 2]" in output assert "[1,_2]" in output - output, _ = run_cmd("hy", "(hello-world)") + output, _ = run_cmd("hy -i", "(hello-world)") assert "(hello-world)" not in output assert "1 + 1" in output assert "2" in output - output, _ = run_cmd("hy", "#rad") + output, _ = run_cmd("hy -i", "#rad") assert "#rad" not in output assert "'totally' + 'rad'" in output assert "'totallyrad'" in output - output, _ = run_cmd("hy --repl-output-fn repr", "[1 2 3 4]") + output, _ = run_cmd("hy -i --repl-output-fn repr", "[1 2 3 4]") assert "[1, 2, 3, 4]" in output assert "[1 2 3 4]" not in output assert "[1,_2,_3,_4]" not in output # spy == False and custom repl-output-fn os.environ["HYSTARTUP"] = "tests/resources/spy_off_startup.hy" - output, _ = run_cmd("hy --spy", "[1 2]") # overwrite spy with cmdline arg + output, _ = run_cmd("hy -i --spy", "[1 2]") # overwrite spy with cmdline arg assert "[1, 2]" in output assert "[1,~2]" in output @@ -645,11 +536,10 @@ def test_output_buffering(tmp_path): (import sys pathlib [Path]) (print :file sys.stderr (.strip (.read-text (Path #[=[{tf}]=])))) (print "line 2")''') - pf = shlex.quote(str(pf)) - for flag, expected in ("", ""), ("--unbuffered", "line 1"): + for flags, expected in ([], ""), (["--unbuffered"], "line 1"): with open(tf, "wb") as o: - _, stderr = run_cmd(f"hy {flag} {pf}", stdout=o) + _, stderr = run_cmd(["hy", *flags, pf], stdout=o) assert stderr.strip() == expected assert tf.read_text().splitlines() == ["line 1", "line 2"] @@ -664,9 +554,9 @@ def test_uufileuu(tmp_path, monkeypatch): def file_is(arg, expected_py3_9): expected = expected_py3_9 if PY3_9 else Path(arg) - output, _ = run_cmd("python3 " + shlex.quote(arg + "pyex.py")) + output, _ = run_cmd(["python3", arg + "pyex.py"]) assert output.rstrip() == str(expected / "pyex.py") - output, _ = run_cmd("hy " + shlex.quote(arg + "hyex.hy")) + output, _ = run_cmd(["hy", arg + "hyex.hy"]) assert output.rstrip() == str(expected / "hyex.hy") monkeypatch.chdir(tmp_path) @@ -711,3 +601,112 @@ def test_assert(tmp_path, monkeypatch): show_msg = has_msg and not optim and not test assert ("msging" in out) == show_msg assert ("bye" in err) == show_msg + + +def test_hy2py_stdin(): + out, _ = run_cmd("hy2py", "(+ 482 223)") + assert "482 + 223" in out + assert "705" not in out + + +def test_hy2py_compile_only(monkeypatch): + def check(args): + output, _ = run_cmd(f"hy2py {args}") + assert not re.search(r"^hello world$", output, re.M) + + monkeypatch.chdir('tests/resources') + check("hello_world.hy") + check("-m hello_world") + + monkeypatch.chdir('..') + check("resources/hello_world.hy") + check("-m resources.hello_world") + + +def test_hy2py_recursive(monkeypatch, tmp_path): + (tmp_path / 'foo').mkdir() + (tmp_path / 'foo/__init__.py').touch() + (tmp_path / "foo/first.hy").write_text(""" + (import foo.folder.second [a b]) + (print a) + (print b)""") + (tmp_path / "foo/folder").mkdir() + (tmp_path / "foo/folder/__init__.py").touch() + (tmp_path / "foo/folder/second.hy").write_text(""" + (setv a 1) + (setv b "hello world")""") + + monkeypatch.chdir(tmp_path) + + _, err = run_cmd("hy2py -m foo", expect=1) + assert "ValueError" in err + + run_cmd("hy2py -m foo --output bar") + assert set((tmp_path / 'bar').rglob('*')) == { + tmp_path / 'bar' / p + for p in ('first.py', 'folder', 'folder/second.py')} + + output, _ = run_cmd("python3 first.py", cwd = tmp_path / 'bar') + assert output == "1\nhello world\n" + + +@pytest.mark.parametrize('case', ['hy -m', 'hy2py -m']) +def test_relative_require(case, monkeypatch, tmp_path): + # https://github.com/hylang/hy/issues/2204 + + (tmp_path / 'pkg').mkdir() + (tmp_path / 'pkg' / '__init__.py').touch() + (tmp_path / 'pkg' / 'a.hy').write_text(''' + (defmacro m [] + '(setv x (.upper "hello")))''') + (tmp_path / 'pkg' / 'b.hy').write_text(''' + (require .a [m]) + (m) + (print x)''') + monkeypatch.chdir(tmp_path) + + if case == 'hy -m': + output, _ = run_cmd('hy -m pkg.b') + elif case == 'hy2py -m': + run_cmd('hy2py -m pkg -o out') + (tmp_path / 'out' / '__init__.py').touch() + output, _ = run_cmd('python3 -m out.b') + + assert 'HELLO' in output + + +def test_require_doesnt_pollute_core(monkeypatch, tmp_path): + # https://github.com/hylang/hy/issues/1978 + """Macros loaded from an external module should not pollute + `_hy_macros` with macros from core.""" + + (tmp_path / 'aaa.hy').write_text(''' + (defmacro foo [] + '(setv x (.upper "argelfraster")))''') + (tmp_path / 'bbb.hy').write_text(''' + (require aaa :as A) + (A.foo) + (print + x + (not-in "if" _hy_macros) + (not-in "cond" _hy_macros))''') + # `if` is a result macro; `cond` is a regular macro. + monkeypatch.chdir(tmp_path) + + # Try it without and then with bytecode. + for _ in (1, 2): + assert 'ARGELFRASTER True True' in run_cmd('hy bbb.hy')[0] + + +def test_run_dir_or_zip(tmp_path): + + (tmp_path / 'dir').mkdir() + (tmp_path / 'dir' / '__main__.hy').write_text('(print (+ "A" "Z"))') + out, _ = run_cmd(['hy', tmp_path / 'dir']) + assert 'AZ' in out + + from zipfile import ZipFile + with ZipFile(tmp_path / 'zoom.zip', 'w') as o: + o.writestr('__main__.hy', '(print (+ "B" "Y"))') + out, _ = run_cmd(['hy', tmp_path / 'zoom.zip']) + assert 'BY' in out diff --git a/tests/test_completer.py b/tests/test_completer.py index 5ca3a7771..e58f2bc2a 100644 --- a/tests/test_completer.py +++ b/tests/test_completer.py @@ -25,6 +25,13 @@ def test_history_custom_location(tmp_path): readline.add_history(expected_entry) actual_entry = history_location.read_text() + + # yes, this is recommended way to check GNU readline vs libedit + # see https://docs.python.org/3.11/library/readline.html + if "libedit" in readline.__doc__: + # libedit saves spaces as octal escapes, so convert them back + actual_entry = actual_entry.replace("\\040", " ") + assert expected_entry in actual_entry diff --git a/tests/test_hy2py.py b/tests/test_hy2py.py index 5a993be92..b88cd08a6 100644 --- a/tests/test_hy2py.py +++ b/tests/test_hy2py.py @@ -1,10 +1,13 @@ import asyncio import itertools import math -import os + +import pytest import hy.importer from hy import mangle +from hy._compat import PYODIDE +from tests.resources import can_test_async def test_direct_import(): @@ -13,19 +16,22 @@ def test_direct_import(): assert_stuff(tests.resources.pydemo) +@pytest.mark.skipif(PYODIDE, reason="subprocess.check_call not implemented on Pyodide") def test_hy2py_import(): import contextlib import os import subprocess path = "tests/resources/pydemo_as_py.py" + env = dict(os.environ) + env["PYTHONIOENCODING"] = "UTF-8" + env["PYTHONPATH"] = "." + os.pathsep + env.get("PYTHONPATH", "") try: with open(path, "wb") as o: subprocess.check_call( ["hy2py", "tests/resources/pydemo.hy"], stdout=o, - env={**os.environ, "PYTHONIOENCODING": "UTF-8"}, - ) + env=env) import tests.resources.pydemo_as_py as m finally: with contextlib.suppress(FileNotFoundError): @@ -148,7 +154,8 @@ class C: assert m.pys_accum == [0, 1, 2, 3, 4] assert m.py_accum == "01234" - assert asyncio.run(m.coro()) == list("abcdef") + if can_test_async: + assert asyncio.run(m.coro()) == list("abcdef") assert m.cheese == [1, 1] assert m.mac_results == ["x", "x"] diff --git a/tests/test_models.py b/tests/test_models.py index d1de2ce82..9ad851545 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,14 +23,13 @@ replace_hy_obj, ) -hy.models.COLORED = False - def test_symbol_or_keyword(): for x in ("foo", "foo-bar", "foo_bar", "✈é😂⁂"): assert str(Symbol(x)) == x assert Keyword(x).name == x - for x in ("", ":foo", "5"): + for x in ("", ":foo", "5", "#foo"): + # https://github.com/hylang/hy/issues/2383 with pytest.raises(ValueError): Symbol(x) assert Keyword(x).name == x diff --git a/tests/test_reader.py b/tests/test_reader.py index c2e76efe0..0ef34dc57 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -23,8 +23,8 @@ from hy.reader.exceptions import LexException, PrematureEndOfInput -def tokenize(s): - return list(read_many(s)) +def tokenize(*args, **kwargs): + return list(read_many(*args, **kwargs)) def peoi(): @@ -48,7 +48,7 @@ def check_trace_output(capsys, execinfo, expected): hy_exc_handler(execinfo.type, execinfo.value, execinfo.tb) captured_w_filtering = capsys.readouterr()[-1].strip("\n") - output = [x.rstrip() for x in captured_w_filtering.split("\n")] + output = [x.rstrip() for x in captured_w_filtering.split("\n") if "^^^" not in x] # Make sure the filtered frames aren't the same as the unfiltered ones. assert output != captured_wo_filtering.split("\n") @@ -86,7 +86,7 @@ def test_lex_single_quote_err(): ' File "", line 1', " '", " ^", - "hy.reader.exceptions.LexException: Could not identify the next token.", + "hy.reader.exceptions.PrematureEndOfInput: Premature end of input while attempting to parse one form", ], ) @@ -97,6 +97,26 @@ def test_lex_expression_symbols(): assert objs == [Expression([Symbol("foo"), Symbol("bar")])] +def test_symbol_and_sugar(): + # https://github.com/hylang/hy/issues/1798 + + s = Symbol + + def e(*x): + return Expression(x) + + for char, head in ( + ("'", "quote"), + ("`", "quasiquote"), + ("~", "unquote"), + ("~@", "unquote-splice"), + ): + for string in (f"a{s1}{char}{s2}b" for s1 in ("", " ") for s2 in ("", " ")): + assert tokenize(string) == [s("a"), e(s(head), s("b"))] + + assert tokenize("a~ @b") == tokenize("a ~ @b") == [s("a"), e(s("unquote"), s("@b"))] + + def test_lex_expression_strings(): """Test that expressions can produce strings""" objs = tokenize('(foo "bar")') @@ -139,9 +159,9 @@ def test_lex_strings_exception(): ' File "", line 1', ' "\\x8"', " ^", - "hy.reader.exceptions.LexException: (unicode error)" + "hy.reader.exceptions.LexException:" " 'unicodeescape' codec can't decode bytes in position 0-2:" - " truncated \\xXX escape (, line 1)", + " truncated \\xXX escape", ], ) @@ -158,9 +178,10 @@ def test_lex_bracket_strings(): def test_lex_integers(): - """Make sure that integers are valid expressions""" - objs = tokenize("42 ") - assert objs == [Integer(42)] + assert tokenize("42") == [Integer(42)] + assert tokenize("0x80") == [Integer(128)] + assert tokenize("0o1232") == [Integer(666)] + assert tokenize("0b1011101") == [Integer(93)] def test_lex_expression_float(): @@ -204,9 +225,13 @@ def t(x): def f(x): return [Expression([Symbol("foo"), x])] + assert t("2j") == f(Complex(2.0j)) + assert t("2J") == f(Complex(2.0j)) assert t("2.j") == f(Complex(2.0j)) + assert t("2.J") == f(Complex(2.0j)) assert t("-0.5j") == f(Complex(-0.5j)) assert t("1.e7j") == f(Complex(1e7j)) + assert t("1.e7J") == f(Complex(1e7j)) assert t("j") == f(Symbol("j")) assert t("J") == f(Symbol("J")) assert isnan(t("NaNj")[0][1].imag) @@ -215,6 +240,13 @@ def f(x): assert t("Inf-Infj") == f(Complex(complex(float("inf"), float("-inf")))) assert t("Inf-INFj") == f(Symbol("Inf-INFj")) + # https://github.com/hylang/hy/issues/2521 + assert isnan(t("NaNJ")[0][1].imag) + assert t("nanJ") == f(Symbol("nanJ")) + assert t("InfJ") == f(Complex(complex(0, float("inf")))) + assert t("iNfJ") == f(Symbol("iNfJ")) + assert t("Inf-INFJ") == f(Symbol("Inf-INFJ")) + def test_lex_digit_separators(): @@ -222,17 +254,20 @@ def test_lex_digit_separators(): assert tokenize("1,000,000") == [Integer(1000000)] assert tokenize("1,000_000") == [Integer(1000000)] assert tokenize("1_000,000") == [Integer(1000000)] + # https://github.com/hylang/hy/issues/1340 + assert tokenize("_42") == [Symbol("_42")] assert tokenize("0x_af") == [Integer(0xAF)] assert tokenize("0x,af") == [Integer(0xAF)] + assert tokenize("0_xaf") == [Integer(0xAF)] assert tokenize("0b_010") == [Integer(0b010)] assert tokenize("0b,010") == [Integer(0b010)] assert tokenize("0o_373") == [Integer(0o373)] assert tokenize("0o,373") == [Integer(0o373)] - assert tokenize("1_2.3,4") == [Float(12.34)] - assert tokenize("1_2e3,4") == [Float(12e34)] - assert tokenize("1,0_00j") == [Complex(1000j)] + assert tokenize("1_2._3,4") == [Float(12.34)] + assert tokenize("1_2e_3,4") == [Float(12e34)] + assert tokenize("1,0_00j,") == [Complex(1000j)] assert tokenize("1,,,,___,____,,__,,2__,,,__") == [Integer(12)] assert tokenize("_1,,,,___,____,,__,,2__,,,__") == [ @@ -243,6 +278,26 @@ def test_lex_digit_separators(): ] +def test_leading_zero(): + assert tokenize("0") == [Integer(0)] + assert tokenize("0000") == [Integer(0)] + assert tokenize("010") == [Integer(10)] + assert tokenize("000010") == [Integer(10)] + assert tokenize("000010.00") == [Float(10)] + assert tokenize("010+000010j") == [Complex(10 + 10j)] + + +def test_dotted_identifiers(): + t = tokenize + + assert t("foo.bar") == t("(. foo bar)") + assert t("foo.bar.baz") == t("(. foo bar baz)") + assert t(".foo") == t("(. None foo)") + assert t(".foo.bar.baz") == t("(. None foo bar baz)") + assert t("..foo") == t("(.. None foo)") + assert t("..foo.bar.baz") == t("(.. None foo bar baz)") + + def test_lex_bad_attrs(): with lexe() as execinfo: tokenize("1.foo") @@ -252,9 +307,7 @@ def test_lex_bad_attrs(): ' File "", line 1', " 1.foo", " ^", - "hy.reader.exceptions.LexException: Cannot access attribute on anything other" - " than a name (in order to get attributes of expressions," - " use `(. )` or `(. )`)", + "hy.reader.exceptions.LexException: The parts of a dotted identifier must be symbols", ], ) @@ -275,6 +328,24 @@ def test_lex_bad_attrs(): tokenize(":hello.foo") +def test_lists(): + assert tokenize("[1 2 3 4]") == [List(map(Integer, (1, 2, 3, 4)))] + + +def test_dicts(): + assert tokenize("{1 2 3 4}") == [Dict(map(Integer, (1, 2, 3, 4)))] + assert tokenize("{1 (+ 1 1) 3 (+ 2 2)}") == [ + Dict( + ( + Integer(1), + Expression((Symbol("+"), Integer(1), Integer(1))), + Integer(3), + Expression((Symbol("+"), Integer(2), Integer(2))), + ) + ) + ] + + def test_lex_column_counting(): entry = tokenize("(foo (one two))")[0] assert entry.start_line == 1 @@ -351,6 +422,13 @@ def test_lex_line_counting_multi_inner(): assert inner.start_column == 5 +def test_line_counting_dotted(): + # https://github.com/hylang/hy/issues/2422 + x, = tokenize(";;;;;\na.b") + for e in (x, *x): + assert e.start_line == 2 + + def test_dicts(): """Ensure that we can tokenize a dict.""" objs = tokenize("{foo bar bar baz}") @@ -493,77 +571,77 @@ def test_discard(): # empty assert tokenize("") == [] # single - assert tokenize("#_1") == [] + assert tokenize("#_ 1") == [] # multiple - assert tokenize("#_1 #_2") == [] - assert tokenize("#_1 #_2 #_3") == [] + assert tokenize("#_ 1 #_ 2") == [] + assert tokenize("#_ 1 #_ 2 #_ 3") == [] # nested discard - assert tokenize("#_ #_1 2") == [] - assert tokenize("#_ #_ #_1 2 3") == [] + assert tokenize("#_ #_ 1 2") == [] + assert tokenize("#_ #_ #_ 1 2 3") == [] # trailing assert tokenize("0") == [Integer(0)] - assert tokenize("0 #_1") == [Integer(0)] - assert tokenize("0 #_1 #_2") == [Integer(0)] + assert tokenize("0 #_ 1") == [Integer(0)] + assert tokenize("0 #_ 1 #_ 2") == [Integer(0)] # leading assert tokenize("2") == [Integer(2)] - assert tokenize("#_1 2") == [Integer(2)] - assert tokenize("#_0 #_1 2") == [Integer(2)] - assert tokenize("#_ #_0 1 2") == [Integer(2)] + assert tokenize("#_ 1 2") == [Integer(2)] + assert tokenize("#_ 0 #_ 1 2") == [Integer(2)] + assert tokenize("#_ #_ 0 1 2") == [Integer(2)] # both - assert tokenize("#_1 2 #_3") == [Integer(2)] - assert tokenize("#_0 #_1 2 #_ #_3 4") == [Integer(2)] + assert tokenize("#_ 1 2 #_ 3") == [Integer(2)] + assert tokenize("#_ 0 #_ 1 2 #_ #_ 3 4") == [Integer(2)] # inside - assert tokenize("0 #_1 2") == [Integer(0), Integer(2)] - assert tokenize("0 #_1 #_2 3") == [Integer(0), Integer(3)] - assert tokenize("0 #_ #_1 2 3") == [Integer(0), Integer(3)] + assert tokenize("0 #_ 1 2") == [Integer(0), Integer(2)] + assert tokenize("0 #_ 1 #_ 2 3") == [Integer(0), Integer(3)] + assert tokenize("0 #_ #_ 1 2 3") == [Integer(0), Integer(3)] # in List assert tokenize("[]") == [List([])] - assert tokenize("[#_1]") == [List([])] - assert tokenize("[#_1 #_2]") == [List([])] - assert tokenize("[#_ #_1 2]") == [List([])] + assert tokenize("[#_ 1]") == [List([])] + assert tokenize("[#_ 1 #_ 2]") == [List([])] + assert tokenize("[#_ #_ 1 2]") == [List([])] assert tokenize("[0]") == [List([Integer(0)])] - assert tokenize("[0 #_1]") == [List([Integer(0)])] - assert tokenize("[0 #_1 #_2]") == [List([Integer(0)])] + assert tokenize("[0 #_ 1]") == [List([Integer(0)])] + assert tokenize("[0 #_ 1 #_ 2]") == [List([Integer(0)])] assert tokenize("[2]") == [List([Integer(2)])] - assert tokenize("[#_1 2]") == [List([Integer(2)])] - assert tokenize("[#_0 #_1 2]") == [List([Integer(2)])] - assert tokenize("[#_ #_0 1 2]") == [List([Integer(2)])] + assert tokenize("[#_ 1 2]") == [List([Integer(2)])] + assert tokenize("[#_ 0 #_ 1 2]") == [List([Integer(2)])] + assert tokenize("[#_ #_ 0 1 2]") == [List([Integer(2)])] # in Set assert tokenize("#{}") == [Set()] - assert tokenize("#{#_1}") == [Set()] - assert tokenize("#{0 #_1}") == [Set([Integer(0)])] - assert tokenize("#{#_1 0}") == [Set([Integer(0)])] + assert tokenize("#{#_ 1}") == [Set()] + assert tokenize("#{0 #_ 1}") == [Set([Integer(0)])] + assert tokenize("#{#_ 1 0}") == [Set([Integer(0)])] # in Dict assert tokenize("{}") == [Dict()] - assert tokenize("{#_1}") == [Dict()] - assert tokenize("{#_0 1 2}") == [Dict([Integer(1), Integer(2)])] - assert tokenize("{1 #_0 2}") == [Dict([Integer(1), Integer(2)])] - assert tokenize("{1 2 #_0}") == [Dict([Integer(1), Integer(2)])] + assert tokenize("{#_ 1}") == [Dict()] + assert tokenize("{#_ 0 1 2}") == [Dict([Integer(1), Integer(2)])] + assert tokenize("{1 #_ 0 2}") == [Dict([Integer(1), Integer(2)])] + assert tokenize("{1 2 #_ 0}") == [Dict([Integer(1), Integer(2)])] # in Expression assert tokenize("()") == [Expression()] - assert tokenize("(#_foo)") == [Expression()] - assert tokenize("(#_foo bar)") == [Expression([Symbol("bar")])] - assert tokenize("(foo #_bar)") == [Expression([Symbol("foo")])] + assert tokenize("(#_ foo)") == [Expression()] + assert tokenize("(#_ foo bar)") == [Expression([Symbol("bar")])] + assert tokenize("(foo #_ bar)") == [Expression([Symbol("foo")])] assert tokenize("(foo :bar 1)") == [ Expression([Symbol("foo"), Keyword("bar"), Integer(1)]) ] - assert tokenize("(foo #_:bar 1)") == [Expression([Symbol("foo"), Integer(1)])] - assert tokenize("(foo :bar #_1)") == [Expression([Symbol("foo"), Keyword("bar")])] + assert tokenize("(foo #_ :bar 1)") == [Expression([Symbol("foo"), Integer(1)])] + assert tokenize("(foo :bar #_ 1)") == [Expression([Symbol("foo"), Keyword("bar")])] # discard term with nesting - assert tokenize("[1 2 #_[a b c [d e [f g] h]] 3 4]") == [ + assert tokenize("[1 2 #_ [a b c [d e [f g] h]] 3 4]") == [ List([Integer(1), Integer(2), Integer(3), Integer(4)]) ] # discard with other prefix syntax - assert tokenize("a #_'b c") == [Symbol("a"), Symbol("c")] - assert tokenize("a '#_b c") == [ + assert tokenize("a #_ 'b c") == [Symbol("a"), Symbol("c")] + assert tokenize("a '#_ b c") == [ Symbol("a"), Expression([Symbol("quote"), Symbol("c")]), ] - assert tokenize("a '#_b #_c d") == [ + assert tokenize("a '#_ b #_ c d") == [ Symbol("a"), Expression([Symbol("quote"), Symbol("d")]), ] - assert tokenize("a '#_ #_b c d") == [ + assert tokenize("a '#_ #_ b c d") == [ Symbol("a"), Expression([Symbol("quote"), Symbol("d")]), ] @@ -596,9 +674,7 @@ def test_lex_exception_filtering(capsys): ' File "", line 3', " 1.foo", " ^", - "hy.reader.exceptions.LexException: Cannot access attribute on anything other" - " than a name (in order to get attributes of expressions," - " use `(. )` or `(. )`)", + "hy.reader.exceptions.LexException: The parts of a dotted identifier must be symbols", ], ) @@ -608,13 +684,21 @@ def test_read_error(): pointing to the source position where the error arose.""" import traceback - - from hy.compiler import hy_eval + import hy from hy.errors import HySyntaxError, hy_exc_handler - from hy.reader import read with pytest.raises(HySyntaxError) as e: - hy_eval(read("(do (defn))")) + hy.eval(hy.read("(do (defn))")) assert "".join(traceback.format_exception_only(e.type, e.value)).startswith( ' File "", line 1\n (do (defn))\n ^\n' ) + + +def test_shebang(): + from hy.errors import HySyntaxError + + with pytest.raises(HySyntaxError): + # By default, `read_many` doesn't allow a shebang. + assert tokenize('#!/usr/bin/env hy\n5') + assert (tokenize('#!/usr/bin/env hy\n5', skip_shebang = True) == + [Integer(5)])