From 5d5d6fafa8012365ef8b175b27129249564ef5c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:23:46 +0000 Subject: [PATCH 1/7] Initial plan From 4853b503daec6ebe3d48cef6a021dbbd42a9b02b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:33:07 +0000 Subject: [PATCH 2/7] Add environment variable support for verb AST fallback Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- datar/apis/dplyr.py | 6 +- datar/apis/tibble.py | 6 +- datar/apis/tidyr.py | 6 +- datar/core/verb_env.py | 117 +++++++++++++++++++++ datar/misc.py | 2 +- tests/test_verb_env.py | 158 +++++++++++++++++++++++++++++ tests/test_verb_env_integration.py | 43 ++++++++ 7 files changed, 325 insertions(+), 13 deletions(-) create mode 100644 datar/core/verb_env.py create mode 100644 tests/test_verb_env.py create mode 100644 tests/test_verb_env_integration.py diff --git a/datar/apis/dplyr.py b/datar/apis/dplyr.py index d2b0dfc3b..c6ba6b29f 100644 --- a/datar/apis/dplyr.py +++ b/datar/apis/dplyr.py @@ -7,10 +7,8 @@ TypeVar as _TypeVar, ) -from pipda import ( - register_verb as _register_verb, - register_func as _register_func, -) +from pipda import register_func as _register_func +from ..core.verb_env import register_verb as _register_verb from ..core.defaults import f as _f_symbolic from ..core.utils import ( diff --git a/datar/apis/tibble.py b/datar/apis/tibble.py index f1491e84b..bc506b845 100644 --- a/datar/apis/tibble.py +++ b/datar/apis/tibble.py @@ -1,10 +1,8 @@ from __future__ import annotations as _ from typing import Any, Callable as _Callable -from pipda import ( - register_verb as _register_verb, - register_func as _register_func, -) +from pipda import register_func as _register_func +from ..core.verb_env import register_verb as _register_verb from ..core.utils import ( NotImplementedByCurrentBackendError as _NotImplementedByCurrentBackendError, diff --git a/datar/apis/tidyr.py b/datar/apis/tidyr.py index bea38f46c..150c0ff9a 100644 --- a/datar/apis/tidyr.py +++ b/datar/apis/tidyr.py @@ -1,10 +1,8 @@ from __future__ import annotations as _ from typing import Any, Callable as _Callable, Mapping as _Mapping -from pipda import ( - register_verb as _register_verb, - register_func as _register_func, -) +from pipda import register_func as _register_func +from ..core.verb_env import register_verb as _register_verb from ..core.utils import ( NotImplementedByCurrentBackendError as _NotImplementedByCurrentBackendError, diff --git a/datar/core/verb_env.py b/datar/core/verb_env.py new file mode 100644 index 000000000..7e8e73219 --- /dev/null +++ b/datar/core/verb_env.py @@ -0,0 +1,117 @@ +"""Utilities for handling verb registration with environment variable support""" +from __future__ import annotations + +import os +from functools import wraps +from typing import Callable + +from pipda import register_verb as _pipda_register_verb +from pipda.utils import TypeHolder + + +def _get_ast_fallback_from_env(func_name: str) -> str | None: + """Get ast_fallback value from environment variables. + + Checks for per-verb environment variable first, then falls back to global. + + Args: + func_name: The name of the function being registered + + Returns: + The ast_fallback value from environment variables, or None if not set + """ + # Convert function name to uppercase for environment variable + # e.g., "select" -> "SELECT", "filter_" -> "FILTER" + verb_name = func_name.rstrip("_").upper() + + # Check for per-verb environment variable first + per_verb_key = f"DATAR_{verb_name}_AST_FALLBACK" + per_verb_value = os.environ.get(per_verb_key) + if per_verb_value: + return per_verb_value + + # Fall back to global environment variable + global_key = "DATAR_VERB_AST_FALLBACK" + global_value = os.environ.get(global_key) + if global_value: + return global_value + + return None + + +def register_verb( + cls=TypeHolder, + *, + func: Callable = None, + context=None, + kw_context=None, + name: str = None, + qualname: str = None, + doc: str = None, + module: str = None, + dependent: bool = False, + ast_fallback: str = None, +) -> Callable: + """Register a verb with environment variable support for ast_fallback. + + This is a wrapper around pipda's register_verb that adds support for + environment variables to control the ast_fallback behavior. + + Environment variables: + DATAR_VERB_AST_FALLBACK: Global fallback for all verbs + DATAR__AST_FALLBACK: Per-verb fallback (takes precedence) + + Valid values for ast_fallback: + - "piping": Assume data >> verb(...) calling pattern + - "normal": Assume verb(data, ...) calling pattern + - "piping_warning": Assume piping, show warning (default) + - "normal_warning": Assume normal, show warning + - "raise": Raise an error when AST is not available + + Args: + See pipda.register_verb for parameter documentation. + + Returns: + The registered verb or a decorator to register a verb + """ + # If func is provided directly (not used as decorator) + if func is not None: + env_fallback = _get_ast_fallback_from_env(func.__name__) + if env_fallback and ast_fallback is None: + ast_fallback = env_fallback + + return _pipda_register_verb( + cls, + func=func, + context=context, + kw_context=kw_context, + name=name, + qualname=qualname, + doc=doc, + module=module, + dependent=dependent, + ast_fallback=ast_fallback, + ) + + # When used as a decorator + def decorator(f: Callable) -> Callable: + env_fallback = _get_ast_fallback_from_env(f.__name__) + if env_fallback and ast_fallback is None: + final_ast_fallback = env_fallback + else: + final_ast_fallback = ast_fallback + + return _pipda_register_verb( + cls, + func=f, + context=context, + kw_context=kw_context, + name=name, + qualname=qualname, + doc=doc, + module=module, + dependent=dependent, + ast_fallback=final_ast_fallback, + ) + + return decorator diff --git a/datar/misc.py b/datar/misc.py index e8669734f..e96cd8997 100644 --- a/datar/misc.py +++ b/datar/misc.py @@ -1,6 +1,6 @@ from typing import Any as _Any, Callable as _Callable -from pipda import register_verb as _register_verb +from .core.verb_env import register_verb as _register_verb from .core.load_plugins import plugin as _plugin diff --git a/tests/test_verb_env.py b/tests/test_verb_env.py new file mode 100644 index 000000000..e32dcb391 --- /dev/null +++ b/tests/test_verb_env.py @@ -0,0 +1,158 @@ +"""Tests for verb environment variable support""" +import os +import pytest + + +def test_env_var_global(): + """Test global environment variable DATAR_VERB_AST_FALLBACK""" + # Set the global environment variable + os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" + + try: + # Import after setting the environment variable + # to ensure the verb picks up the setting + from datar.core.verb_env import register_verb, _get_ast_fallback_from_env + + # Test that the function reads the environment variable + result = _get_ast_fallback_from_env("test_verb") + assert result == "piping" + + finally: + # Clean up + del os.environ["DATAR_VERB_AST_FALLBACK"] + + +def test_env_var_per_verb(): + """Test per-verb environment variable DATAR__AST_FALLBACK""" + # Set a per-verb environment variable + os.environ["DATAR_SELECT_AST_FALLBACK"] = "normal" + + try: + from datar.core.verb_env import _get_ast_fallback_from_env + + # Test that the function reads the per-verb environment variable + result = _get_ast_fallback_from_env("select") + assert result == "normal" + + finally: + # Clean up + del os.environ["DATAR_SELECT_AST_FALLBACK"] + + +def test_env_var_per_verb_with_trailing_underscore(): + """Test per-verb environment variable for verbs with trailing underscore""" + # Set a per-verb environment variable for filter_ verb + os.environ["DATAR_FILTER_AST_FALLBACK"] = "raise" + + try: + from datar.core.verb_env import _get_ast_fallback_from_env + + # Test that the function reads the per-verb environment variable + # even when the function name has a trailing underscore + result = _get_ast_fallback_from_env("filter_") + assert result == "raise" + + finally: + # Clean up + del os.environ["DATAR_FILTER_AST_FALLBACK"] + + +def test_env_var_precedence(): + """Test that per-verb environment variable takes precedence over global""" + os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" + os.environ["DATAR_MUTATE_AST_FALLBACK"] = "normal" + + try: + from datar.core.verb_env import _get_ast_fallback_from_env + + # For mutate, the per-verb setting should take precedence + result = _get_ast_fallback_from_env("mutate") + assert result == "normal" + + # For other verbs, the global setting should be used + result = _get_ast_fallback_from_env("select") + assert result == "piping" + + finally: + # Clean up + del os.environ["DATAR_VERB_AST_FALLBACK"] + del os.environ["DATAR_MUTATE_AST_FALLBACK"] + + +def test_env_var_not_set(): + """Test behavior when no environment variable is set""" + # Ensure no relevant environment variables are set + for key in list(os.environ.keys()): + if key.startswith("DATAR_") and key.endswith("_AST_FALLBACK"): + del os.environ[key] + + from datar.core.verb_env import _get_ast_fallback_from_env + + # Should return None when no environment variable is set + result = _get_ast_fallback_from_env("test_verb") + assert result is None + + +def test_register_verb_with_env_var(): + """Test that register_verb respects environment variables""" + os.environ["DATAR_VERB_AST_FALLBACK"] = "normal" + + try: + from datar.core.verb_env import register_verb + + # Define a simple test verb + @register_verb() + def test_verb(data): + """Test verb""" + return data + + # The verb should be registered (we can't easily check the ast_fallback + # without accessing internals, but we can verify it doesn't error) + assert callable(test_verb) + + finally: + # Clean up + del os.environ["DATAR_VERB_AST_FALLBACK"] + + +def test_register_verb_explicit_ast_fallback_takes_precedence(): + """Test that explicit ast_fallback parameter takes precedence over env vars""" + os.environ["DATAR_VERB_AST_FALLBACK"] = "normal" + + try: + from datar.core.verb_env import register_verb + + # Define a test verb with explicit ast_fallback + @register_verb(ast_fallback="raise") + def test_verb_explicit(data): + """Test verb with explicit ast_fallback""" + return data + + # The verb should be registered with the explicit value + # We can't easily verify the ast_fallback value without accessing internals, + # but we can verify it doesn't error + assert callable(test_verb_explicit) + + finally: + # Clean up + del os.environ["DATAR_VERB_AST_FALLBACK"] + + +def test_register_verb_with_dependent(): + """Test that register_verb works with dependent parameter""" + os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" + + try: + from datar.core.verb_env import register_verb + + # Define a dependent verb + @register_verb(dependent=True) + def test_dependent_verb(data): + """Test dependent verb""" + return data + + assert callable(test_dependent_verb) + + finally: + # Clean up + del os.environ["DATAR_VERB_AST_FALLBACK"] diff --git a/tests/test_verb_env_integration.py b/tests/test_verb_env_integration.py new file mode 100644 index 000000000..0ee0de323 --- /dev/null +++ b/tests/test_verb_env_integration.py @@ -0,0 +1,43 @@ +"""Integration test to demonstrate the environment variable feature""" +import os +import sys +import subprocess + + +def test_env_var_integration(): + """Integration test showing environment variables in action""" + # Create a simple test script that uses datar + test_script = """ +import os +from datar.core.verb_env import register_verb + +# Set environment variable before importing verbs +os.environ['DATAR_TEST_VERB_AST_FALLBACK'] = 'piping' + +@register_verb() +def test_verb(data): + return data + +# Verify the verb is registered +assert callable(test_verb) +print("SUCCESS: Environment variable integration test passed") +""" + + # Write to a temporary file + with open('/tmp/test_env_integration.py', 'w') as f: + f.write(test_script) + + # Run the script + result = subprocess.run( + [sys.executable, '/tmp/test_env_integration.py'], + capture_output=True, + text=True + ) + + # Check the result + assert result.returncode == 0 + assert "SUCCESS" in result.stdout + + +if __name__ == "__main__": + test_env_var_integration() From 8d280f4eb2986706aa384ba64a59b13dcf597040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:35:31 +0000 Subject: [PATCH 3/7] Fix linting issues and add documentation Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- datar/core/verb_env.py | 31 +++++++------ docs/ENV_VARS.md | 99 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 docs/ENV_VARS.md diff --git a/datar/core/verb_env.py b/datar/core/verb_env.py index 7e8e73219..7b99e0810 100644 --- a/datar/core/verb_env.py +++ b/datar/core/verb_env.py @@ -2,7 +2,6 @@ from __future__ import annotations import os -from functools import wraps from typing import Callable from pipda import register_verb as _pipda_register_verb @@ -11,31 +10,31 @@ def _get_ast_fallback_from_env(func_name: str) -> str | None: """Get ast_fallback value from environment variables. - + Checks for per-verb environment variable first, then falls back to global. - + Args: func_name: The name of the function being registered - + Returns: The ast_fallback value from environment variables, or None if not set """ # Convert function name to uppercase for environment variable # e.g., "select" -> "SELECT", "filter_" -> "FILTER" verb_name = func_name.rstrip("_").upper() - + # Check for per-verb environment variable first per_verb_key = f"DATAR_{verb_name}_AST_FALLBACK" per_verb_value = os.environ.get(per_verb_key) if per_verb_value: return per_verb_value - + # Fall back to global environment variable global_key = "DATAR_VERB_AST_FALLBACK" global_value = os.environ.get(global_key) if global_value: return global_value - + return None @@ -53,24 +52,24 @@ def register_verb( ast_fallback: str = None, ) -> Callable: """Register a verb with environment variable support for ast_fallback. - + This is a wrapper around pipda's register_verb that adds support for environment variables to control the ast_fallback behavior. - + Environment variables: DATAR_VERB_AST_FALLBACK: Global fallback for all verbs DATAR__AST_FALLBACK: Per-verb fallback (takes precedence) - + Valid values for ast_fallback: - "piping": Assume data >> verb(...) calling pattern - "normal": Assume verb(data, ...) calling pattern - "piping_warning": Assume piping, show warning (default) - "normal_warning": Assume normal, show warning - "raise": Raise an error when AST is not available - + Args: See pipda.register_verb for parameter documentation. - + Returns: The registered verb or a decorator to register a verb """ @@ -79,7 +78,7 @@ def register_verb( env_fallback = _get_ast_fallback_from_env(func.__name__) if env_fallback and ast_fallback is None: ast_fallback = env_fallback - + return _pipda_register_verb( cls, func=func, @@ -92,7 +91,7 @@ def register_verb( dependent=dependent, ast_fallback=ast_fallback, ) - + # When used as a decorator def decorator(f: Callable) -> Callable: env_fallback = _get_ast_fallback_from_env(f.__name__) @@ -100,7 +99,7 @@ def decorator(f: Callable) -> Callable: final_ast_fallback = env_fallback else: final_ast_fallback = ast_fallback - + return _pipda_register_verb( cls, func=f, @@ -113,5 +112,5 @@ def decorator(f: Callable) -> Callable: dependent=dependent, ast_fallback=final_ast_fallback, ) - + return decorator diff --git a/docs/ENV_VARS.md b/docs/ENV_VARS.md new file mode 100644 index 000000000..4ee891188 --- /dev/null +++ b/docs/ENV_VARS.md @@ -0,0 +1,99 @@ +# Environment Variable Support for Verb AST Fallback + +This document explains how to use environment variables to control the fallback behavior for verbs when AST (Abstract Syntax Tree) is not retrievable. + +## Overview + +When the AST of a function call is not available (e.g., in some interactive environments or when code is executed dynamically), datar verbs need to make assumptions about how they were called. You can now control this behavior using environment variables. + +## Environment Variables + +### Global Configuration + +Set the global fallback behavior for all verbs: + +```bash +export DATAR_VERB_AST_FALLBACK="piping" +``` + +This will assume all datar verbs are called with the piping pattern: `data >> verb(...)` + +### Per-Verb Configuration + +You can also configure individual verbs: + +```bash +# Assume select is called like: data >> select(...) +export DATAR_SELECT_AST_FALLBACK="piping" + +# Assume mutate is called like: mutate(data, ...) +export DATAR_MUTATE_AST_FALLBACK="normal" +``` + +Per-verb environment variables take precedence over the global setting. + +## Valid Values + +The following values are supported for ast_fallback: + +- `"piping"`: Assume `data >> verb(...)` calling pattern +- `"normal"`: Assume `verb(data, ...)` calling pattern +- `"piping_warning"`: Assume piping call, but show a warning (default) +- `"normal_warning"`: Assume normal call, but show a warning +- `"raise"`: Raise an error when AST is not available + +## Examples + +### Example 1: Set Global Piping Mode + +Add to your `.bashrc` or shell configuration: + +```bash +export DATAR_VERB_AST_FALLBACK="piping" +``` + +Then in Python: + +```python +from datar.all import * + +# This will work without warnings when AST is not available +iris >> select(f.Species, f.Sepal_Length) +``` + +### Example 2: Mixed Configuration + +```bash +# Most verbs use piping +export DATAR_VERB_AST_FALLBACK="piping" + +# But filter uses normal calling convention +export DATAR_FILTER_AST_FALLBACK="normal" +``` + +Then in Python: + +```python +from datar.all import * + +# select uses piping (from global setting) +iris >> select(f.Species) + +# filter uses normal calling (from per-verb setting) +filter(iris, f.Sepal_Length > 5) +``` + +### Example 3: Raise Errors for Debugging + +```bash +export DATAR_VERB_AST_FALLBACK="raise" +``` + +This will help you identify cases where AST is not available, which can be useful for debugging. + +## Notes + +- Environment variables are checked when the verb is registered (at import time). +- If you change environment variables after importing datar modules, you'll need to restart your Python session for the changes to take effect. +- If you explicitly specify `ast_fallback` in the `@register_verb()` decorator, it takes precedence over environment variables. +- Verb names with trailing underscores (e.g., `filter_`) should use the environment variable without the underscore (e.g., `DATAR_FILTER_AST_FALLBACK`). From 9aab68799167ba0e4e71f91299983292e112156c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:38:08 +0000 Subject: [PATCH 4/7] Address code review feedback: fix temp file handling and reduce code duplication Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- datar/core/verb_env.py | 21 +++++++++--------- tests/test_verb_env_integration.py | 34 ++++++++++++++++++------------ 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/datar/core/verb_env.py b/datar/core/verb_env.py index 7b99e0810..b0446ec74 100644 --- a/datar/core/verb_env.py +++ b/datar/core/verb_env.py @@ -73,12 +73,16 @@ def register_verb( Returns: The registered verb or a decorator to register a verb """ + def _apply_env_fallback(f: Callable, fallback: str = None) -> str: + """Apply environment variable fallback if not explicitly set.""" + if fallback is None: + env_fallback = _get_ast_fallback_from_env(f.__name__) + return env_fallback if env_fallback else fallback + return fallback + # If func is provided directly (not used as decorator) if func is not None: - env_fallback = _get_ast_fallback_from_env(func.__name__) - if env_fallback and ast_fallback is None: - ast_fallback = env_fallback - + final_ast_fallback = _apply_env_fallback(func, ast_fallback) return _pipda_register_verb( cls, func=func, @@ -89,17 +93,12 @@ def register_verb( doc=doc, module=module, dependent=dependent, - ast_fallback=ast_fallback, + ast_fallback=final_ast_fallback, ) # When used as a decorator def decorator(f: Callable) -> Callable: - env_fallback = _get_ast_fallback_from_env(f.__name__) - if env_fallback and ast_fallback is None: - final_ast_fallback = env_fallback - else: - final_ast_fallback = ast_fallback - + final_ast_fallback = _apply_env_fallback(f, ast_fallback) return _pipda_register_verb( cls, func=f, diff --git a/tests/test_verb_env_integration.py b/tests/test_verb_env_integration.py index 0ee0de323..06f770e51 100644 --- a/tests/test_verb_env_integration.py +++ b/tests/test_verb_env_integration.py @@ -2,6 +2,7 @@ import os import sys import subprocess +import tempfile def test_env_var_integration(): @@ -22,21 +23,28 @@ def test_verb(data): assert callable(test_verb) print("SUCCESS: Environment variable integration test passed") """ - + # Write to a temporary file - with open('/tmp/test_env_integration.py', 'w') as f: + with tempfile.NamedTemporaryFile( + mode='w', suffix='.py', delete=False + ) as f: + temp_path = f.name f.write(test_script) - - # Run the script - result = subprocess.run( - [sys.executable, '/tmp/test_env_integration.py'], - capture_output=True, - text=True - ) - - # Check the result - assert result.returncode == 0 - assert "SUCCESS" in result.stdout + + try: + # Run the script + result = subprocess.run( + [sys.executable, temp_path], + capture_output=True, + text=True + ) + + # Check the result + assert result.returncode == 0 + assert "SUCCESS" in result.stdout + finally: + # Clean up the temporary file + os.unlink(temp_path) if __name__ == "__main__": From aa8b40dcbae98c9025c1dd34b0516b3df46de594 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 04:54:23 +0000 Subject: [PATCH 5/7] Simplify implementation per feedback - use helper function instead of wrapper Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- datar/apis/dplyr.py | 171 ++++++++++++++++------------- datar/apis/tibble.py | 27 +++-- datar/apis/tidyr.py | 43 ++++---- datar/core/verb_env.py | 97 ++-------------- datar/misc.py | 5 +- tests/test_verb_env.py | 120 ++++++++------------ tests/test_verb_env_integration.py | 5 +- 7 files changed, 194 insertions(+), 274 deletions(-) diff --git a/datar/apis/dplyr.py b/datar/apis/dplyr.py index c6ba6b29f..f4066ed65 100644 --- a/datar/apis/dplyr.py +++ b/datar/apis/dplyr.py @@ -7,8 +7,11 @@ TypeVar as _TypeVar, ) -from pipda import register_func as _register_func -from ..core.verb_env import register_verb as _register_verb +from pipda import ( + register_verb as _register_verb, + register_func as _register_func, +) +from ..core.verb_env import get_verb_ast_fallback as _get_verb_ast_fallback from ..core.defaults import f as _f_symbolic from ..core.utils import ( @@ -19,7 +22,7 @@ T = _TypeVar("T") -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("pick")) def pick(_data: T, *args) -> T: """Pick columns by name @@ -36,7 +39,7 @@ def pick(_data: T, *args) -> T: raise _NotImplementedByCurrentBackendError("pick", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("across")) def across(_data: T, *args, _names=None, **kwargs) -> T: """Apply the same transformation to multiple columns @@ -85,7 +88,7 @@ def across(_data: T, *args, _names=None, **kwargs) -> T: raise _NotImplementedByCurrentBackendError("across", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("c_across")) def c_across(_data: T, _cols=None) -> T: """Apply the same transformation to multiple columns rowwisely @@ -99,7 +102,7 @@ def c_across(_data: T, _cols=None) -> T: raise _NotImplementedByCurrentBackendError("c_across", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("if_any")) def if_any(_data, *args, _names=None, **kwargs) -> Any: """Apply the same predicate function to a selection of columns and combine the results True if any element is True. @@ -110,7 +113,7 @@ def if_any(_data, *args, _names=None, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("if_any", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("if_all")) def if_all(_data, *args, _names=None, **kwargs) -> Any: """Apply the same predicate function to a selection of columns and combine the results True if all elements are True. @@ -121,7 +124,7 @@ def if_all(_data, *args, _names=None, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("if_all", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("symdiff")) def symdiff(x: T, y: T) -> T: """Get the symmetric difference of two dataframes @@ -141,7 +144,7 @@ def symdiff(x: T, y: T) -> T: raise _NotImplementedByCurrentBackendError("symdiff", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("arrange")) def arrange(_data, *args, _by_group=False, **kwargs) -> Any: """orders the rows of a data frame by the values of selected columns. @@ -228,7 +231,7 @@ def cur_column(_data, _name) -> Any: raise _NotImplementedByCurrentBackendError("cur_column") -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("cur_data")) def cur_data(_data) -> Any: """Get the current dataframe @@ -241,7 +244,7 @@ def cur_data(_data) -> Any: raise _NotImplementedByCurrentBackendError("cur_data", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("n")) def n(_data) -> Any: """Get the current group size @@ -254,7 +257,9 @@ def n(_data) -> Any: raise _NotImplementedByCurrentBackendError("n", _data) -@_register_verb(dependent=True) +@_register_verb( + dependent=True, ast_fallback=_get_verb_ast_fallback("cur_data_all") +) def cur_data_all(_data) -> Any: """Get the current data for the current group including the grouping variables @@ -268,7 +273,7 @@ def cur_data_all(_data) -> Any: raise _NotImplementedByCurrentBackendError("cur_data_all", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("cur_group")) def cur_group(_data) -> Any: """Get the current group @@ -281,7 +286,9 @@ def cur_group(_data) -> Any: raise _NotImplementedByCurrentBackendError("cur_group", _data) -@_register_verb(dependent=True) +@_register_verb( + dependent=True, ast_fallback=_get_verb_ast_fallback("cur_group_id") +) def cur_group_id(_data) -> Any: """Get the current group id @@ -294,7 +301,9 @@ def cur_group_id(_data) -> Any: raise _NotImplementedByCurrentBackendError("cur_group_id", _data) -@_register_verb(dependent=True) +@_register_verb( + dependent=True, ast_fallback=_get_verb_ast_fallback("cur_group_rows") +) def cur_group_rows(_data) -> Any: """Get the current group row indices @@ -308,7 +317,7 @@ def cur_group_rows(_data) -> Any: # count_tally -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("count")) def count( _data, *args, @@ -345,7 +354,7 @@ def count( raise _NotImplementedByCurrentBackendError("count", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("tally")) def tally(_data, wt=None, sort=False, name=None) -> Any: """Count the number of rows in each group @@ -369,7 +378,7 @@ def tally(_data, wt=None, sort=False, name=None) -> Any: raise _NotImplementedByCurrentBackendError("tally", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("add_count")) def add_count(_data, *args, wt=None, sort=False, name="n", **kwargs) -> Any: """Add a count column to a data frame @@ -396,7 +405,7 @@ def add_count(_data, *args, wt=None, sort=False, name="n", **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("add_count", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("add_tally")) def add_tally(_data, wt=None, sort=False, name="n") -> Any: """Add a count column to a data frame @@ -440,7 +449,7 @@ def desc(x) -> Any: # filter -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("filter_")) def filter_(_data, *conditions, _preserve: bool = False) -> Any: """Filter a data frame based on conditions @@ -459,7 +468,7 @@ def filter_(_data, *conditions, _preserve: bool = False) -> Any: # distinct -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("distinct")) def distinct( _data, *args, @@ -483,7 +492,7 @@ def distinct( raise _NotImplementedByCurrentBackendError("distinct", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("n_distinct")) def n_distinct(_data, na_rm: bool = True) -> Any: """Count the number of distinct values @@ -501,7 +510,7 @@ def n_distinct(_data, na_rm: bool = True) -> Any: # glimpse -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("glimpse")) def glimpse(_data, width: int = None, formatter=None) -> Any: """Display a summary of a data frame @@ -517,7 +526,7 @@ def glimpse(_data, width: int = None, formatter=None) -> Any: # slice -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_")) def slice_(_data, *args, _preserve: bool = False) -> Any: """Extract rows by their position @@ -535,7 +544,7 @@ def slice_(_data, *args, _preserve: bool = False) -> Any: raise _NotImplementedByCurrentBackendError("slice", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_head")) def slice_head(_data, n: int = None, prop: float = None) -> Any: """Extract the first rows @@ -553,7 +562,7 @@ def slice_head(_data, n: int = None, prop: float = None) -> Any: raise _NotImplementedByCurrentBackendError("slice_head", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_tail")) def slice_tail(_data, n: int = None, prop: float = None) -> Any: """Extract the last rows @@ -571,7 +580,7 @@ def slice_tail(_data, n: int = None, prop: float = None) -> Any: raise _NotImplementedByCurrentBackendError("slice_tail", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_sample")) def slice_sample( _data, n: int = 1, @@ -597,7 +606,7 @@ def slice_sample( raise _NotImplementedByCurrentBackendError("slice_sample", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_min")) def slice_min( _data, order_by, @@ -625,7 +634,7 @@ def slice_min( raise _NotImplementedByCurrentBackendError("slice_min", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("slice_max")) def slice_max( _data, order_by, @@ -849,7 +858,7 @@ def last(x, order_by=None, default=None) -> Any: # group_by -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_by")) def group_by(_data, *args, _add: bool = False, _drop: bool = None) -> Any: """Create a grouped frame @@ -868,7 +877,7 @@ def group_by(_data, *args, _add: bool = False, _drop: bool = None) -> Any: raise _NotImplementedByCurrentBackendError("group_by", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("ungroup")) def ungroup(_data, *cols: str | int) -> Any: """Remove grouping variables @@ -885,7 +894,7 @@ def ungroup(_data, *cols: str | int) -> Any: raise _NotImplementedByCurrentBackendError("ungroup", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rowwise")) def rowwise(_data, *cols: str | int) -> Any: """Create a rowwise frame @@ -902,7 +911,7 @@ def rowwise(_data, *cols: str | int) -> Any: raise _NotImplementedByCurrentBackendError("rowwise", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_by_drop_default")) def group_by_drop_default(_data) -> Any: """Get the default value of `_drop` of a frame @@ -918,7 +927,7 @@ def group_by_drop_default(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_by_drop_default", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_vars")) def group_vars(_data) -> Any: """Get the grouping variables of a frame @@ -934,7 +943,7 @@ def group_vars(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_vars", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_indices")) def group_indices(_data) -> Any: """Get the group indices of a frame @@ -950,7 +959,7 @@ def group_indices(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_indices", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_keys")) def group_keys(_data) -> Any: """Get the group keys of a frame @@ -966,7 +975,7 @@ def group_keys(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_keys", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_size")) def group_size(_data) -> Any: """Get the group sizes of a frame @@ -982,7 +991,7 @@ def group_size(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_size", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_rows")) def group_rows(_data) -> Any: """Get the group rows of a frame @@ -998,7 +1007,7 @@ def group_rows(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_rows", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_cols")) def group_cols(_data) -> Any: """Get the group columns of a frame @@ -1014,7 +1023,7 @@ def group_cols(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_cols", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_data")) def group_data(_data) -> Any: """Get the group data of a frame @@ -1030,7 +1039,7 @@ def group_data(_data) -> Any: raise _NotImplementedByCurrentBackendError("group_data", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("n_groups")) def n_groups(_data) -> int: """Get the number of groups of a frame @@ -1046,7 +1055,7 @@ def n_groups(_data) -> int: raise _NotImplementedByCurrentBackendError("n_groups", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_map")) def group_map(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: """Apply a function to each group @@ -1066,7 +1075,7 @@ def group_map(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("group_map", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_modify")) def group_modify(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: """Apply a function to each group @@ -1086,7 +1095,7 @@ def group_modify(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("group_modify", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_split")) def group_split(_data, *args, _keep: bool = False, **kwargs) -> Any: """Split a grouped frame into a list of data frames @@ -1105,7 +1114,7 @@ def group_split(_data, *args, _keep: bool = False, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("group_split", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_trim")) def group_trim(_data, _drop=None) -> Any: """Remove empty groups @@ -1122,7 +1131,7 @@ def group_trim(_data, _drop=None) -> Any: raise _NotImplementedByCurrentBackendError("group_trim", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("group_walk")) def group_walk(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: """Apply a function to each group @@ -1141,7 +1150,7 @@ def group_walk(_data, _f, *args, _keep: bool = False, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("group_walk", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("with_groups")) def with_groups(_data, _groups, _func, *args, **kwargs) -> Any: """Modify the grouping variables for a single operation. @@ -1212,7 +1221,7 @@ def case_when(cond, value, *more_cases) -> Any: # join -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("inner_join")) def inner_join( x, y, @@ -1263,7 +1272,7 @@ def inner_join( raise _NotImplementedByCurrentBackendError("inner_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("left_join")) def left_join( x, y, @@ -1314,7 +1323,7 @@ def left_join( raise _NotImplementedByCurrentBackendError("left_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("right_join")) def right_join( x, y, @@ -1365,7 +1374,7 @@ def right_join( raise _NotImplementedByCurrentBackendError("right_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("full_join")) def full_join( x, y, @@ -1416,7 +1425,7 @@ def full_join( raise _NotImplementedByCurrentBackendError("full_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("semi_join")) def semi_join( x, y, @@ -1445,7 +1454,7 @@ def semi_join( raise _NotImplementedByCurrentBackendError("semi_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("anti_join")) def anti_join( x, y, @@ -1474,7 +1483,7 @@ def anti_join( raise _NotImplementedByCurrentBackendError("anti_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("nest_join")) def nest_join( x, y, @@ -1512,7 +1521,7 @@ def nest_join( raise _NotImplementedByCurrentBackendError("nest_join", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("cross_join")) def cross_join( x: T, y: T, @@ -1577,7 +1586,7 @@ def lag(x, n=1, default=None, order_by=None) -> Any: # mutate -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("mutate")) def mutate( _data, *args, _keep: str = "all", _before=None, _after=None, **kwargs ) -> Any: @@ -1624,7 +1633,7 @@ def mutate( raise _NotImplementedByCurrentBackendError("mutate", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("transmute")) def transmute(_data, *args, _before=None, _after=None, **kwargs) -> Any: """Add new columns to a data frame and remove existing columns using mutate with `_keep="none"`. @@ -1708,7 +1717,7 @@ def with_order(order, func, x, *args, **kwargs) -> Any: # pull -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("pull")) def pull(_data, var: str | int = -1, name=None, to=None) -> Any: """Pull a series or a dataframe from a dataframe @@ -1942,7 +1951,7 @@ def recode_factor( raise _NotImplementedByCurrentBackendError("recode_factor") -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("relocate")) def relocate( _data, *args, @@ -1975,7 +1984,7 @@ def relocate( raise _NotImplementedByCurrentBackendError("relocate", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rename")) def rename(_data, **kwargs) -> Any: """Rename columns @@ -1992,7 +2001,7 @@ def rename(_data, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("rename", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rename_with")) def rename_with(_data, _fn, *args, **kwargs) -> Any: """Rename columns with a function @@ -2015,7 +2024,7 @@ def rename_with(_data, _fn, *args, **kwargs) -> Any: # rows -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_insert")) def rows_insert( x, y, @@ -2049,7 +2058,7 @@ def rows_insert( raise _NotImplementedByCurrentBackendError("rows_insert", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_update")) def rows_update( x, y, @@ -2087,7 +2096,7 @@ def rows_update( raise _NotImplementedByCurrentBackendError("rows_update", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_patch")) def rows_patch( x, y, @@ -2125,7 +2134,7 @@ def rows_patch( raise _NotImplementedByCurrentBackendError("rows_patch", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_upsert")) def rows_upsert(x, y, by=None, **kwargs) -> Any: """Upsert rows in x with values from y @@ -2151,7 +2160,7 @@ def rows_upsert(x, y, by=None, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("rows_upsert", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_delete")) def rows_delete( x, y, @@ -2189,7 +2198,7 @@ def rows_delete( raise _NotImplementedByCurrentBackendError("rows_delete", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rows_append")) def rows_append(x, y, **kwargs) -> Any: """Append rows in y to x @@ -2208,7 +2217,7 @@ def rows_append(x, y, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("rows_append", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("select")) def select(_data, *args, **kwargs) -> Any: """Select columns from a data frame. @@ -2243,7 +2252,7 @@ def union_all(x, y) -> Any: raise _NotImplementedByCurrentBackendError("union_all", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("summarise")) def summarise(_data, *args, _groups: str = None, **kwargs) -> Any: """Summarise a data frame. @@ -2270,7 +2279,7 @@ def summarise(_data, *args, _groups: str = None, **kwargs) -> Any: summarize = summarise -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("reframe")) def reframe(_data, *args, **kwargs) -> Any: """Reframe a data frame. @@ -2289,7 +2298,7 @@ def reframe(_data, *args, **kwargs) -> Any: raise _NotImplementedByCurrentBackendError("reframe", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("where")) def where(_data, fn: _Callable) -> Any: """Selects the variables for which a function returns True. @@ -2308,7 +2317,9 @@ def where(_data, fn: _Callable) -> Any: raise _NotImplementedByCurrentBackendError("where", _data) -@_register_verb(dependent=True) +@_register_verb( + dependent=True, ast_fallback=_get_verb_ast_fallback("everything") +) def everything(_data) -> Any: """Select all variables. @@ -2324,7 +2335,7 @@ def everything(_data) -> Any: raise _NotImplementedByCurrentBackendError("everything", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("last_col")) def last_col(_data, offset: int = 0, vars=None) -> Any: """Select the last column. @@ -2342,7 +2353,9 @@ def last_col(_data, offset: int = 0, vars=None) -> Any: raise _NotImplementedByCurrentBackendError("last_col", _data) -@_register_verb(dependent=True) +@_register_verb( + dependent=True, ast_fallback=_get_verb_ast_fallback("starts_with") +) def starts_with(_data, match, ignore_case: bool = True, vars=None) -> Any: """Select columns that start with a string. @@ -2361,7 +2374,7 @@ def starts_with(_data, match, ignore_case: bool = True, vars=None) -> Any: raise _NotImplementedByCurrentBackendError("starts_with", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("ends_with")) def ends_with(_data, match, ignore_case: bool = True, vars=None) -> Any: """Select columns that end with a string. @@ -2380,7 +2393,7 @@ def ends_with(_data, match, ignore_case: bool = True, vars=None) -> Any: raise _NotImplementedByCurrentBackendError("ends_with", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("contains")) def contains(_data, match, ignore_case: bool = True, vars=None) -> Any: """Select columns that contain a string. @@ -2399,7 +2412,7 @@ def contains(_data, match, ignore_case: bool = True, vars=None) -> Any: raise _NotImplementedByCurrentBackendError("contains", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("matches")) def matches(_data, match, ignore_case: bool = True, vars=None) -> Any: """Select columns that match a regular expression. @@ -2435,7 +2448,7 @@ def num_range(prefix: str, range_, width: int = None) -> Any: raise _NotImplementedByCurrentBackendError("num_range") -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("all_of")) def all_of(_data, x) -> Any: """For strict selection. @@ -2456,7 +2469,7 @@ def all_of(_data, x) -> Any: raise _NotImplementedByCurrentBackendError("all_of", _data) -@_register_verb(dependent=True) +@_register_verb(dependent=True, ast_fallback=_get_verb_ast_fallback("any_of")) def any_of(_data, x, vars=None) -> Any: """For strict selection. diff --git a/datar/apis/tibble.py b/datar/apis/tibble.py index bc506b845..47e5bd314 100644 --- a/datar/apis/tibble.py +++ b/datar/apis/tibble.py @@ -1,8 +1,11 @@ from __future__ import annotations as _ from typing import Any, Callable as _Callable -from pipda import register_func as _register_func -from ..core.verb_env import register_verb as _register_verb +from pipda import ( + register_verb as _register_verb, + register_func as _register_func, +) +from ..core.verb_env import get_verb_ast_fallback as _get_verb_ast_fallback from ..core.utils import ( NotImplementedByCurrentBackendError as _NotImplementedByCurrentBackendError, @@ -110,13 +113,13 @@ def tibble_row( raise _NotImplementedByCurrentBackendError("tibble_row") -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("as_tibble")) def as_tibble(df) -> Any: """Convert a DataFrame object to Tibble object""" raise _NotImplementedByCurrentBackendError("as_tibble", df) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("enframe")) def enframe(x, name="name", value="value") -> Any: """Converts mappings or lists to one- or two-column data frames. @@ -134,7 +137,7 @@ def enframe(x, name="name", value="value") -> Any: raise _NotImplementedByCurrentBackendError("enframe", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("deframe")) def deframe(x) -> Any: """Converts two-column data frames to a dictionary using the first column as name and the second column as value. @@ -149,7 +152,7 @@ def deframe(x) -> Any: raise _NotImplementedByCurrentBackendError("deframe", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("add_row")) def add_row( _data, *args, @@ -176,7 +179,7 @@ def add_row( raise _NotImplementedByCurrentBackendError("add_row", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("add_column")) def add_column( _data, *args, @@ -204,7 +207,7 @@ def add_column( raise _NotImplementedByCurrentBackendError("add_column", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("has_rownames")) def has_rownames(_data) -> bool: """Detect if a data frame has row names @@ -220,7 +223,7 @@ def has_rownames(_data) -> bool: raise _NotImplementedByCurrentBackendError("has_rownames", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("remove_rownames")) def remove_rownames(_data) -> Any: """Remove the index/rownames of a data frame @@ -236,7 +239,7 @@ def remove_rownames(_data) -> Any: raise _NotImplementedByCurrentBackendError("remove_rownames", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rownames_to_column")) def rownames_to_column(_data, var="rowname") -> Any: """Add rownames as a column @@ -253,7 +256,7 @@ def rownames_to_column(_data, var="rowname") -> Any: raise _NotImplementedByCurrentBackendError("rownames_to_column", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("rowid_to_column")) def rowid_to_column(_data, var="rowid") -> Any: """Add rownames as a column @@ -268,7 +271,7 @@ def rowid_to_column(_data, var="rowid") -> Any: raise _NotImplementedByCurrentBackendError("rowid_to_column", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("column_to_rownames")) def column_to_rownames(_data, var="rowname") -> Any: """Set rownames/index with one column, and remove it diff --git a/datar/apis/tidyr.py b/datar/apis/tidyr.py index 150c0ff9a..057dcdf81 100644 --- a/datar/apis/tidyr.py +++ b/datar/apis/tidyr.py @@ -1,8 +1,11 @@ from __future__ import annotations as _ from typing import Any, Callable as _Callable, Mapping as _Mapping -from pipda import register_func as _register_func -from ..core.verb_env import register_verb as _register_verb +from pipda import ( + register_verb as _register_verb, + register_func as _register_func, +) +from ..core.verb_env import get_verb_ast_fallback as _get_verb_ast_fallback from ..core.utils import ( NotImplementedByCurrentBackendError as _NotImplementedByCurrentBackendError, @@ -26,7 +29,7 @@ def full_seq(x, period, tol=1e-6) -> Any: raise _NotImplementedByCurrentBackendError("full_seq", x) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("chop")) def chop( data, cols=None, @@ -44,7 +47,7 @@ def chop( raise _NotImplementedByCurrentBackendError("chop", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("unchop")) def unchop( data, cols=None, @@ -87,7 +90,7 @@ def unchop( raise _NotImplementedByCurrentBackendError("unchop", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("nest")) def nest( _data, _names_sep: str = None, @@ -111,7 +114,7 @@ def nest( raise _NotImplementedByCurrentBackendError("nest", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("unnest")) def unnest( data, *cols: str | int, @@ -156,7 +159,7 @@ def unnest( raise _NotImplementedByCurrentBackendError("unnest", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("pack")) def pack( _data, _names_sep: str = None, @@ -177,7 +180,7 @@ def pack( raise _NotImplementedByCurrentBackendError("pack", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("unpack")) def unpack( data, cols, @@ -211,7 +214,7 @@ def unpack( raise _NotImplementedByCurrentBackendError("unpack", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("expand")) def expand( data, *args, @@ -317,7 +320,7 @@ def crossing( raise _NotImplementedByCurrentBackendError("crossing") -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("complete")) def complete( data, *args, @@ -351,7 +354,7 @@ def complete( raise _NotImplementedByCurrentBackendError("complete", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("drop_na")) def drop_na( _data, *columns: str, @@ -375,7 +378,7 @@ def drop_na( raise _NotImplementedByCurrentBackendError("drop_na", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("extract")) def extract( data, col: str | int, @@ -407,7 +410,7 @@ def extract( raise _NotImplementedByCurrentBackendError("extract", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("fill")) def fill( _data, *columns: str | int, @@ -432,7 +435,7 @@ def fill( raise _NotImplementedByCurrentBackendError("fill", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("pivot_longer")) def pivot_longer( _data, cols, @@ -538,7 +541,7 @@ def pivot_longer( raise _NotImplementedByCurrentBackendError("pivot_longer", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("pivot_wider")) def pivot_wider( _data, id_cols=None, @@ -591,7 +594,7 @@ def pivot_wider( raise _NotImplementedByCurrentBackendError("pivot_wider", _data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("separate")) def separate( data, col: int | str, @@ -638,7 +641,7 @@ def separate( raise _NotImplementedByCurrentBackendError("separate", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("separate_rows")) def separate_rows( data, *columns: str, @@ -660,7 +663,7 @@ def separate_rows( raise _NotImplementedByCurrentBackendError("separate_rows", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("uncount")) def uncount( data, weights, @@ -683,7 +686,7 @@ def uncount( raise _NotImplementedByCurrentBackendError("uncount", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("unite")) def unite( data, col: str, @@ -709,7 +712,7 @@ def unite( raise _NotImplementedByCurrentBackendError("unite", data) -@_register_verb() +@_register_verb(ast_fallback=_get_verb_ast_fallback("replace_na")) def replace_na( data, data_or_replace=None, diff --git a/datar/core/verb_env.py b/datar/core/verb_env.py index b0446ec74..12aa15a84 100644 --- a/datar/core/verb_env.py +++ b/datar/core/verb_env.py @@ -1,29 +1,31 @@ -"""Utilities for handling verb registration with environment variable support""" +"""Utilities for getting verb AST fallback from environment variables""" from __future__ import annotations import os -from typing import Callable -from pipda import register_verb as _pipda_register_verb -from pipda.utils import TypeHolder - -def _get_ast_fallback_from_env(func_name: str) -> str | None: +def get_verb_ast_fallback(verb: str) -> str | None: """Get ast_fallback value from environment variables. Checks for per-verb environment variable first, then falls back to global. Args: - func_name: The name of the function being registered + verb: The name of the verb (e.g., "mutate", "select", "filter") Returns: The ast_fallback value from environment variables, or None if not set + + Example: + >>> @register_verb(ast_fallback=get_verb_ast_fallback("mutate")) + >>> def mutate(...): + ... pass """ - # Convert function name to uppercase for environment variable + # Convert verb name to uppercase, removing trailing underscore if present # e.g., "select" -> "SELECT", "filter_" -> "FILTER" - verb_name = func_name.rstrip("_").upper() + verb_name = verb.rstrip("_").upper() # Check for per-verb environment variable first + # e.g., DATAR_MUTATE_AST_FALLBACK per_verb_key = f"DATAR_{verb_name}_AST_FALLBACK" per_verb_value = os.environ.get(per_verb_key) if per_verb_value: @@ -36,80 +38,3 @@ def _get_ast_fallback_from_env(func_name: str) -> str | None: return global_value return None - - -def register_verb( - cls=TypeHolder, - *, - func: Callable = None, - context=None, - kw_context=None, - name: str = None, - qualname: str = None, - doc: str = None, - module: str = None, - dependent: bool = False, - ast_fallback: str = None, -) -> Callable: - """Register a verb with environment variable support for ast_fallback. - - This is a wrapper around pipda's register_verb that adds support for - environment variables to control the ast_fallback behavior. - - Environment variables: - DATAR_VERB_AST_FALLBACK: Global fallback for all verbs - DATAR__AST_FALLBACK: Per-verb fallback (takes precedence) - - Valid values for ast_fallback: - - "piping": Assume data >> verb(...) calling pattern - - "normal": Assume verb(data, ...) calling pattern - - "piping_warning": Assume piping, show warning (default) - - "normal_warning": Assume normal, show warning - - "raise": Raise an error when AST is not available - - Args: - See pipda.register_verb for parameter documentation. - - Returns: - The registered verb or a decorator to register a verb - """ - def _apply_env_fallback(f: Callable, fallback: str = None) -> str: - """Apply environment variable fallback if not explicitly set.""" - if fallback is None: - env_fallback = _get_ast_fallback_from_env(f.__name__) - return env_fallback if env_fallback else fallback - return fallback - - # If func is provided directly (not used as decorator) - if func is not None: - final_ast_fallback = _apply_env_fallback(func, ast_fallback) - return _pipda_register_verb( - cls, - func=func, - context=context, - kw_context=kw_context, - name=name, - qualname=qualname, - doc=doc, - module=module, - dependent=dependent, - ast_fallback=final_ast_fallback, - ) - - # When used as a decorator - def decorator(f: Callable) -> Callable: - final_ast_fallback = _apply_env_fallback(f, ast_fallback) - return _pipda_register_verb( - cls, - func=f, - context=context, - kw_context=kw_context, - name=name, - qualname=qualname, - doc=doc, - module=module, - dependent=dependent, - ast_fallback=final_ast_fallback, - ) - - return decorator diff --git a/datar/misc.py b/datar/misc.py index e96cd8997..8e4a63261 100644 --- a/datar/misc.py +++ b/datar/misc.py @@ -1,13 +1,14 @@ from typing import Any as _Any, Callable as _Callable -from .core.verb_env import register_verb as _register_verb +from pipda import register_verb as _register_verb +from .core.verb_env import get_verb_ast_fallback as _get_verb_ast_fallback from .core.load_plugins import plugin as _plugin locals().update(_plugin.hooks.misc_api()) -@_register_verb(object) +@_register_verb(object, ast_fallback=_get_verb_ast_fallback("pipe")) def pipe(data: _Any, func: _Callable, *args, **kwargs) -> _Any: """Apply a function to the data diff --git a/tests/test_verb_env.py b/tests/test_verb_env.py index e32dcb391..6e2dc9ca3 100644 --- a/tests/test_verb_env.py +++ b/tests/test_verb_env.py @@ -7,16 +7,14 @@ def test_env_var_global(): """Test global environment variable DATAR_VERB_AST_FALLBACK""" # Set the global environment variable os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" - + try: - # Import after setting the environment variable - # to ensure the verb picks up the setting - from datar.core.verb_env import register_verb, _get_ast_fallback_from_env - + from datar.core.verb_env import get_verb_ast_fallback + # Test that the function reads the environment variable - result = _get_ast_fallback_from_env("test_verb") + result = get_verb_ast_fallback("test_verb") assert result == "piping" - + finally: # Clean up del os.environ["DATAR_VERB_AST_FALLBACK"] @@ -26,14 +24,14 @@ def test_env_var_per_verb(): """Test per-verb environment variable DATAR__AST_FALLBACK""" # Set a per-verb environment variable os.environ["DATAR_SELECT_AST_FALLBACK"] = "normal" - + try: - from datar.core.verb_env import _get_ast_fallback_from_env - + from datar.core.verb_env import get_verb_ast_fallback + # Test that the function reads the per-verb environment variable - result = _get_ast_fallback_from_env("select") + result = get_verb_ast_fallback("select") assert result == "normal" - + finally: # Clean up del os.environ["DATAR_SELECT_AST_FALLBACK"] @@ -43,15 +41,15 @@ def test_env_var_per_verb_with_trailing_underscore(): """Test per-verb environment variable for verbs with trailing underscore""" # Set a per-verb environment variable for filter_ verb os.environ["DATAR_FILTER_AST_FALLBACK"] = "raise" - + try: - from datar.core.verb_env import _get_ast_fallback_from_env - + from datar.core.verb_env import get_verb_ast_fallback + # Test that the function reads the per-verb environment variable # even when the function name has a trailing underscore - result = _get_ast_fallback_from_env("filter_") + result = get_verb_ast_fallback("filter_") assert result == "raise" - + finally: # Clean up del os.environ["DATAR_FILTER_AST_FALLBACK"] @@ -61,18 +59,18 @@ def test_env_var_precedence(): """Test that per-verb environment variable takes precedence over global""" os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" os.environ["DATAR_MUTATE_AST_FALLBACK"] = "normal" - + try: - from datar.core.verb_env import _get_ast_fallback_from_env - + from datar.core.verb_env import get_verb_ast_fallback + # For mutate, the per-verb setting should take precedence - result = _get_ast_fallback_from_env("mutate") + result = get_verb_ast_fallback("mutate") assert result == "normal" - + # For other verbs, the global setting should be used - result = _get_ast_fallback_from_env("select") + result = get_verb_ast_fallback("select") assert result == "piping" - + finally: # Clean up del os.environ["DATAR_VERB_AST_FALLBACK"] @@ -85,74 +83,50 @@ def test_env_var_not_set(): for key in list(os.environ.keys()): if key.startswith("DATAR_") and key.endswith("_AST_FALLBACK"): del os.environ[key] - - from datar.core.verb_env import _get_ast_fallback_from_env - + + from datar.core.verb_env import get_verb_ast_fallback + # Should return None when no environment variable is set - result = _get_ast_fallback_from_env("test_verb") + result = get_verb_ast_fallback("test_verb") assert result is None -def test_register_verb_with_env_var(): - """Test that register_verb respects environment variables""" +def test_verb_with_env_var(): + """Test that verbs can use the helper function""" os.environ["DATAR_VERB_AST_FALLBACK"] = "normal" - + try: - from datar.core.verb_env import register_verb - - # Define a simple test verb - @register_verb() + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback + + # Define a simple test verb using the helper + @register_verb(ast_fallback=get_verb_ast_fallback("test_verb")) def test_verb(data): """Test verb""" return data - - # The verb should be registered (we can't easily check the ast_fallback - # without accessing internals, but we can verify it doesn't error) + + # The verb should be registered assert callable(test_verb) - + finally: # Clean up del os.environ["DATAR_VERB_AST_FALLBACK"] -def test_register_verb_explicit_ast_fallback_takes_precedence(): - """Test that explicit ast_fallback parameter takes precedence over env vars""" +def test_explicit_ast_fallback_with_env_var(): + """Test that explicit ast_fallback is used even when env var is set""" os.environ["DATAR_VERB_AST_FALLBACK"] = "normal" - + try: - from datar.core.verb_env import register_verb - - # Define a test verb with explicit ast_fallback - @register_verb(ast_fallback="raise") - def test_verb_explicit(data): - """Test verb with explicit ast_fallback""" - return data - - # The verb should be registered with the explicit value - # We can't easily verify the ast_fallback value without accessing internals, - # but we can verify it doesn't error - assert callable(test_verb_explicit) - - finally: - # Clean up - del os.environ["DATAR_VERB_AST_FALLBACK"] + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback + # When we explicitly pass an ast_fallback, it should be used + # But if we use the helper, it will return the env var value + # This test verifies the helper returns the env var + result = get_verb_ast_fallback("test_verb") + assert result == "normal" -def test_register_verb_with_dependent(): - """Test that register_verb works with dependent parameter""" - os.environ["DATAR_VERB_AST_FALLBACK"] = "piping" - - try: - from datar.core.verb_env import register_verb - - # Define a dependent verb - @register_verb(dependent=True) - def test_dependent_verb(data): - """Test dependent verb""" - return data - - assert callable(test_dependent_verb) - finally: # Clean up del os.environ["DATAR_VERB_AST_FALLBACK"] diff --git a/tests/test_verb_env_integration.py b/tests/test_verb_env_integration.py index 06f770e51..3cfcd924c 100644 --- a/tests/test_verb_env_integration.py +++ b/tests/test_verb_env_integration.py @@ -10,12 +10,13 @@ def test_env_var_integration(): # Create a simple test script that uses datar test_script = """ import os -from datar.core.verb_env import register_verb +from pipda import register_verb +from datar.core.verb_env import get_verb_ast_fallback # Set environment variable before importing verbs os.environ['DATAR_TEST_VERB_AST_FALLBACK'] = 'piping' -@register_verb() +@register_verb(ast_fallback=get_verb_ast_fallback("test_verb")) def test_verb(data): return data From cfba7c40981780238a3c5b1cbbc668fc2a409c42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 05:10:47 +0000 Subject: [PATCH 6/7] Add ENV_VARS.md to mkdocs navigation and link from options page Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- docs/options.md | 2 ++ mkdocs.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/options.md b/docs/options.md index b344eb496..6953c6058 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1,5 +1,7 @@ Options are used to change some behaviors in `datar`. +For environment variable configuration (such as controlling verb AST fallback behavior), see [Environment Variables](ENV_VARS.md). + ## Available options ### allow_conflict_names diff --git a/mkdocs.yml b/mkdocs.yml index 6a5816f8d..fd21abc88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ nav: - 'Import datar': 'import.md' - 'Backends': 'backends.md' - 'Options': 'options.md' + - 'Environment Variables': 'ENV_VARS.md' - 'The f-expression': 'f.md' - 'Data': 'data.md' - 'Examples': From 5cc103dc5c198dde0c63435cea3da7290bc7bdbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 05:15:51 +0000 Subject: [PATCH 7/7] Improve integration tests with exec-based AST fallback behavior testing Co-authored-by: pwwang <1188067+pwwang@users.noreply.github.com> --- tests/test_verb_env_integration.py | 157 ++++++++++++++++++++++------- 1 file changed, 119 insertions(+), 38 deletions(-) diff --git a/tests/test_verb_env_integration.py b/tests/test_verb_env_integration.py index 3cfcd924c..be92f3400 100644 --- a/tests/test_verb_env_integration.py +++ b/tests/test_verb_env_integration.py @@ -1,52 +1,133 @@ """Integration test to demonstrate the environment variable feature""" import os -import sys -import subprocess -import tempfile +import pytest -def test_env_var_integration(): - """Integration test showing environment variables in action""" - # Create a simple test script that uses datar - test_script = """ -import os -from pipda import register_verb -from datar.core.verb_env import get_verb_ast_fallback +def test_verb_ast_fallback_piping(): + """Test that DATAR_*_AST_FALLBACK works with piping mode""" + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback + + # Set environment variable for piping mode + os.environ['DATAR_PLUS_AST_FALLBACK'] = 'piping' + + try: + # Register a simple verb + @register_verb(ast_fallback=get_verb_ast_fallback("plus")) + def plus(x, y): + return x + y + + # Test with exec to disable source code detection at runtime + # In piping mode, piping call should work + result = {} + exec("result['val'] = 1 >> plus(1)", {"plus": plus, "result": result}) + assert result['val'] == 2 + + # Normal call in piping mode returns a placeholder when AST is not available + result = {} + exec("result['val'] = plus(1, 1)", {"plus": plus, "result": result}) + # The result is a placeholder object, not the actual computation + assert str(result['val']) == 'plus(., 1, 1)' + + finally: + del os.environ['DATAR_PLUS_AST_FALLBACK'] + + +def test_verb_ast_fallback_normal(): + """Test that DATAR_*_AST_FALLBACK works with normal mode""" + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback + + # Set environment variable for normal mode + os.environ['DATAR_MINUS_AST_FALLBACK'] = 'normal' + + try: + # Register a simple verb + @register_verb(ast_fallback=get_verb_ast_fallback("minus")) + def minus(x, y): + return x - y + + # Test with exec to disable source code detection at runtime + # In normal mode, normal call should work + result = {} + exec("result['val'] = minus(5, 3)", {"minus": minus, "result": result}) + assert result['val'] == 2 + + # Piping call in normal mode raises TypeError when AST is not available + result = {} + with pytest.raises(TypeError): + exec("result['val'] = 5 >> minus(3)", {"minus": minus, "result": result}) -# Set environment variable before importing verbs -os.environ['DATAR_TEST_VERB_AST_FALLBACK'] = 'piping' + finally: + del os.environ['DATAR_MINUS_AST_FALLBACK'] -@register_verb(ast_fallback=get_verb_ast_fallback("test_verb")) -def test_verb(data): - return data -# Verify the verb is registered -assert callable(test_verb) -print("SUCCESS: Environment variable integration test passed") -""" +def test_verb_ast_fallback_global(): + """Test that DATAR_VERB_AST_FALLBACK works as global fallback""" + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback - # Write to a temporary file - with tempfile.NamedTemporaryFile( - mode='w', suffix='.py', delete=False - ) as f: - temp_path = f.name - f.write(test_script) + # Set global environment variable + os.environ['DATAR_VERB_AST_FALLBACK'] = 'piping' try: - # Run the script - result = subprocess.run( - [sys.executable, temp_path], - capture_output=True, - text=True - ) - - # Check the result - assert result.returncode == 0 - assert "SUCCESS" in result.stdout + # Register verbs without specific env var + @register_verb(ast_fallback=get_verb_ast_fallback("multiply")) + def multiply(x, y): + return x * y + + @register_verb(ast_fallback=get_verb_ast_fallback("divide")) + def divide(x, y): + return x / y + + # Both should use global piping mode + result = {} + exec("result['mul'] = 6 >> multiply(2)", {"multiply": multiply, "result": result}) + assert result['mul'] == 12 + + exec("result['div'] = 10 >> divide(2)", {"divide": divide, "result": result}) + assert result['div'] == 5 + + finally: + del os.environ['DATAR_VERB_AST_FALLBACK'] + + +def test_verb_ast_fallback_precedence(): + """Test that per-verb env var takes precedence over global""" + from pipda import register_verb + from datar.core.verb_env import get_verb_ast_fallback + + # Set global to piping and specific verb to normal + os.environ['DATAR_VERB_AST_FALLBACK'] = 'piping' + os.environ['DATAR_MODULO_AST_FALLBACK'] = 'normal' + + try: + # Register verbs + @register_verb(ast_fallback=get_verb_ast_fallback("power")) + def power(x, y): + return x ** y + + @register_verb(ast_fallback=get_verb_ast_fallback("modulo")) + def modulo(x, y): + return x % y + + # power should use global piping mode + result = {} + exec("result['pow'] = 2 >> power(3)", {"power": power, "result": result}) + assert result['pow'] == 8 + + # modulo should use specific normal mode + exec("result['mod'] = modulo(10, 3)", {"modulo": modulo, "result": result}) + assert result['mod'] == 1 + finally: - # Clean up the temporary file - os.unlink(temp_path) + del os.environ['DATAR_VERB_AST_FALLBACK'] + del os.environ['DATAR_MODULO_AST_FALLBACK'] if __name__ == "__main__": - test_env_var_integration() + test_verb_ast_fallback_piping() + test_verb_ast_fallback_normal() + test_verb_ast_fallback_global() + test_verb_ast_fallback_precedence() + print("All integration tests passed!")