Commit 15296652 authored by Roman Alifanov's avatar Roman Alifanov

Add user decorators, callbacks, and UTF-8 urlencode

- User decorators (@obj.method(args)) for handler registration - Callbacks: pass functions/lambdas as arguments - Fix UTF-8 urlencode with LC_ALL=C - Fix dict field access in class methods - Refactor telegram_echobot to use decorators - Add tests for callbacks and user decorators - Update documentation (LANGUAGE_SPEC, README)
parent d68d4f71
......@@ -145,7 +145,7 @@ result = value | func1 | func2
Декораторы работают как на функциях, так и на методах классов.
**Доступные декораторы:**
**Встроенные декораторы:**
- `@retry(attempts, delay)` — повторные попытки при ошибке
- `@log` — логирование вызовов (выводит в stderr)
- `@cache(ttl)` — кеширование результатов на ttl секунд
......@@ -153,6 +153,38 @@ result = value | func1 | func2
- `@awk` — компиляция в AWK (см. ниже)
- `@test("description")` — пометить функцию как тест (см. ниже)
**Пользовательские декораторы (`@obj.method(args)`):**
Синтаксис `@obj.method(args)` вызывает метод объекта с аргументами декоратора и именем декорируемой функции:
```
class EventBus {
listeners = {}
func on (event, handler) {
this.listeners.set (event, handler)
}
func emit (event, data) {
if this.listeners.has (event) {
handler = this.listeners.get (event)
handler (data)
}
}
}
bus = new EventBus ()
@bus.on ("click")
func on_click (data) {
print ("Clicked: {data}")
}
bus.emit ("click", "button1") # Clicked: button1
```
Используется для регистрации обработчиков, роутинга, событий — как в aiogram, Flask, Express.
```
# На функциях
@retry (attempts = 3, delay = 1)
......@@ -374,6 +406,50 @@ result = filterFn (15) # true
sum = addFn (3, 4) # 7
```
### Колбеки (функции как аргументы)
Функции и лямбды можно передавать как аргументы и вызывать через переменные:
```
func apply (callback, value) {
return callback (value)
}
# Передача обычной функции
func double (x) {
return x * 2
}
result = apply (double, 5) # 10
# Передача лямбды через переменную
triple = x => x * 3
result = apply (triple, 4) # 12
# Передача inline лямбды
result = apply (x => x + 10, 5) # 15
# Колбек с несколькими аргументами
func compute (fn, a, b) {
return fn (a, b)
}
add = (x, y) => x + y
result = compute (add, 3, 7) # 10
```
Колбеки работают в методах классов:
```
class Processor {
func process (callback, value) {
return callback (value)
}
}
p = new Processor ()
square = x => x * x
result = p.process (square, 5) # 25
```
### Циклы
```
......
......@@ -9,7 +9,8 @@
- **Clean syntax** — Python-like readability with Go/Vala influences
- **Classes & inheritance** — OOP with constructors and method calls
- **Lambdas**`x => x * 2`, `(a, b) => a + b`, multiline blocks
- **Decorators**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`
- **Decorators**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`, user decorators `@obj.method()`
- **Callbacks** — pass functions/lambdas as arguments
- **Functional arrays**`.map()`, `.filter()` with lambdas
- **File handles**`fs.open()`, `f.read()`, `f.write()`, `f.close()`
- **Context managers**`with f in fs.open(path) { ... }`
......@@ -127,6 +128,38 @@ func test_add () {
}
```
### User Decorators & Callbacks
User decorators (`@obj.method(args)`) enable handler registration patterns like Flask/aiogram:
```
class Router {
handlers = {}
func route (path, handler) {
this.handlers.set (path, handler)
}
}
router = new Router ()
@router.route ("/home")
func home_handler () {
print ("Home page")
}
```
Functions and lambdas can be passed as callbacks:
```
func apply (callback, value) {
return callback (value)
}
double = x => x * 2
result = apply (double, 5) # 10
result = apply (x => x + 10, 3) # 13
```
### Functional Arrays
```
......@@ -342,14 +375,18 @@ FAIL failing test (1ms)
### Telegram Echo Bot
A simple Telegram bot that echoes messages back (`examples/telegram_echobot/`):
A Telegram bot with user decorators for command routing (`examples/telegram_echobot/`):
```bash
export TELEGRAM_BOT_TOKEN="your_token"
python3 content run examples/telegram_echobot/echobot.ct
python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echobot/bot.ct
```
Uses `json.get()` for parsing Telegram API responses and `str.urlencode()` for URL encoding.
Features:
- User decorators for command registration (`@bot.command("start")`)
- Callbacks for message handlers
- `json.get()` for parsing Telegram API responses
- `str.urlencode()` for UTF-8 URL encoding
## Documentation
......
......@@ -9,7 +9,8 @@
- **Чистый синтаксис** — читаемость Python с влиянием Go/Vala
- **Классы и наследование** — ООП с конструкторами и вызовами методов
- **Лямбды**`x => x * 2`, `(a, b) => a + b`, многострочные блоки
- **Декораторы**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`
- **Декораторы**`@retry`, `@log`, `@cache`, `@validate`, `@awk`, `@test`, пользовательские `@obj.method()`
- **Колбеки** — передача функций/лямбд как аргументов
- **Функциональные массивы**`.map()`, `.filter()` с лямбдами
- **Файловые дескрипторы**`fs.open()`, `f.read()`, `f.write()`, `f.close()`
- **Контекстные менеджеры**`with f in fs.open(path) { ... }`
......@@ -127,6 +128,38 @@ func test_add () {
}
```
### Пользовательские декораторы и колбеки
Пользовательские декораторы (`@obj.method(args)`) позволяют регистрировать обработчики как в Flask/aiogram:
```
class Router {
handlers = {}
func route (path, handler) {
this.handlers.set (path, handler)
}
}
router = new Router ()
@router.route ("/home")
func home_handler () {
print ("Главная страница")
}
```
Функции и лямбды можно передавать как колбеки:
```
func apply (callback, value) {
return callback (value)
}
double = x => x * 2
result = apply (double, 5) # 10
result = apply (x => x + 10, 3) # 13
```
### Функциональные массивы
```
......@@ -342,14 +375,18 @@ FAIL падающий тест (1ms)
### Telegram эхо-бот
Простой Telegram-бот, который отправляет сообщения обратно (`examples/telegram_echobot/`):
Telegram-бот с пользовательскими декораторами для маршрутизации команд (`examples/telegram_echobot/`):
```bash
export TELEGRAM_BOT_TOKEN="your_token"
python3 content run examples/telegram_echobot/echobot.ct
python3 content run examples/telegram_echobot/telegram.ct examples/telegram_echobot/bot.ct
```
Использует `json.get()` для парсинга ответов Telegram API и `str.urlencode()` для URL-кодирования.
Возможности:
- Пользовательские декораторы для регистрации команд (`@bot.command("start")`)
- Колбеки для обработчиков сообщений
- `json.get()` для парсинга ответов Telegram API
- `str.urlencode()` для UTF-8 URL-кодирования
## Документация
......
......@@ -279,6 +279,7 @@ class Parameter:
class Decorator:
name: str = ""
arguments: List[tuple] = field (default_factory=list)
object: Optional[str] = None
location: Optional[SourceLocation] = None
......
......@@ -413,12 +413,15 @@ class ClassMixin:
def generate_function(self, func: FunctionDecl):
test_decorator = None
other_decorators = []
user_decorators = []
builtin_decorators = []
for dec in func.decorators:
if dec.name == "test":
test_decorator = dec
elif dec.object is not None:
user_decorators.append(dec)
else:
other_decorators.append(dec)
builtin_decorators.append(dec)
if test_decorator:
if not self.test_mode:
......@@ -430,21 +433,24 @@ class ClassMixin:
if hasattr(arg_val, 'value'):
description = arg_val.value
self.test_functions.append((func.name, description))
func = FunctionDecl(
name=func.name,
params=func.params,
body=func.body,
decorators=other_decorators,
location=func.location
)
func = FunctionDecl(
name=func.name,
params=func.params,
body=func.body,
decorators=builtin_decorators,
location=func.location
)
for dec in func.decorators:
if dec.name == "awk":
self.generate_awk_function(func)
self._emit_user_decorators(user_decorators, func.name)
return
if func.decorators:
self.generate_decorated_function(func)
self._emit_user_decorators(user_decorators, func.name)
return
name = func.name
......@@ -501,6 +507,28 @@ class ClassMixin:
self.emit("}")
self.emit()
self._emit_user_decorators(user_decorators, func.name)
def _emit_user_decorators(self, decorators: list, func_name: str):
"""Emit calls for user-defined decorators like @bot.command('start')."""
for dec in decorators:
obj = dec.object
method = dec.name
args = []
for arg_name, arg_val in dec.arguments:
val = self.generate_expr(arg_val)
if not val.startswith('"') and not val.startswith('$'):
val = f'"{val}"'
args.append(val)
args.append(f'"{func_name}"')
args_str = " ".join(args)
obj_class = self.instance_vars.get(obj)
if obj_class:
self.emit(f'__ct_class_{obj_class}_{method} "${obj}" {args_str}')
else:
self.emit(f'{obj}_{method} {args_str}')
def generate_decorated_function(self, func: FunctionDecl) -> str:
original_name = f"__ct_orig_{func.name}"
temp_func = FunctionDecl(
......
......@@ -50,6 +50,7 @@ class CodeGenerator(StdlibMixin, AwkCodegenMixin, ExprMixin, StmtMixin,
self.object_vars: Set[str] = set()
self.file_handle_vars: Set[str] = set()
self.nameref_vars: Set[str] = set() # vars that are namerefs to arrays/dicts
self.callback_vars: Set[str] = set() # vars that hold function names (callbacks)
self.instance_vars: Dict[str, str] = {} # var_name -> class_name
self.class_field_types: Dict[tuple, str] = {}
self.func_param_types: Dict[tuple, str] = {} # (func_name, param_name) -> "array"/"dict"
......
......@@ -157,6 +157,12 @@ class UsageAnalyzer:
for dec in stmt.decorators:
if dec.name == 'awk':
self.has_awk = True
if dec.object:
obj_class = self.variable_types.get(dec.object)
if obj_class:
if obj_class not in self.used_methods:
self.used_methods[obj_class] = set()
self.used_methods[obj_class].add(dec.name)
old_func_name = getattr(self, 'current_func_name', None)
self.current_func_name = stmt.name
self._analyze_body(stmt.body)
......
......@@ -67,7 +67,7 @@ class DispatchMixin:
return False
def _generate_call_arg(self, arg) -> str:
"""Generate a single call argument, handling arrays specially."""
"""Generate a single call argument, handling arrays and callbacks specially."""
if isinstance(arg, Identifier):
name = arg.name
param_map = getattr(self, 'param_name_map', {})
......@@ -76,6 +76,10 @@ class DispatchMixin:
return mapped_name
if mapped_name in self.dict_vars or name in self.dict_vars:
return mapped_name
if name in self.functions:
return name
if name in getattr(self, 'callback_vars', set()):
return f'${{{name}}}'
return self.generate_expr(arg)
def _generate_call_args_str(self, arguments: list) -> str:
......@@ -110,6 +114,8 @@ class DispatchMixin:
if isinstance(stmt.value, Lambda):
self.generate_lambda_as_function(stmt.value, target)
self.emit_var_assign(target, target)
self.callback_vars.add(target)
return
if isinstance(stmt.value, NewExpr):
......@@ -304,7 +310,7 @@ class DispatchMixin:
self._validate_type_method("array", method, location)
if field_type == "dict":
if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
dict_ref = f'"${{this}}_{field_name}"'
self.emit(f'{DICT_METHODS_MAP[method]} {dict_ref} {args_str} >/dev/null'.strip())
self._emit_assign_with_op(target, f'${RET_VAR}', stmt.operator)
return True
......@@ -845,8 +851,22 @@ class DispatchMixin:
elif name == "assert_eq":
return f'__ct_assert_eq {args_str}'
else:
if self._is_callback_var(name):
return f'"${{{name}}}" {args_str}'
return f'{name} {args_str}'
def _is_callback_var(self, name: str) -> bool:
"""Check if name is a variable holding a callback (function name)."""
if name in self.functions or name in self.classes:
return False
if name in getattr(self, 'callback_vars', set()):
return True
if name in self.local_vars:
return True
if name in getattr(self, 'current_param_positions', {}):
return True
return False
def _generate_member_call(self, callee: MemberAccess, args: list, args_str: str, location=None) -> str:
"""Generate member access call."""
obj = self.generate_expr(callee.object)
......@@ -958,7 +978,7 @@ class DispatchMixin:
if field_type == "dict":
if method in DICT_METHODS:
dict_ref = f'"${{__CT_OBJ[\\"$this.{field_name}\\"]}}"'
dict_ref = f'"${{this}}_{field_name}"'
return f'{DICT_METHODS_MAP[method]} {dict_ref} {args_str}'.strip()
else:
self._validate_type_method("dict", method, location)
......
......@@ -68,12 +68,25 @@ def compile_files (source_paths: list) -> tuple[bool, str]:
def find_ct_files (directory: str = ".") -> list:
"""Find all .ct files in directory, sorted (main.ct last for proper ordering)."""
"""Find all .ct files in directory, sorted (libraries first, main.ct last)."""
import glob
files = sorted (glob.glob (os.path.join (directory, "*.ct")))
def is_library (path):
basename = os.path.basename (path)
if basename.startswith ("lib") or basename.startswith ("_"):
return True
try:
with open (path, 'r') as f:
content = f.read (2048)
return "class " in content
except:
return False
main_files = [f for f in files if os.path.basename (f) == "main.ct"]
other_files = [f for f in files if os.path.basename (f) != "main.ct"]
return other_files + main_files
lib_files = [f for f in files if f not in main_files and is_library (f)]
other_files = [f for f in files if f not in main_files and f not in lib_files]
return lib_files + other_files + main_files
def cmd_build (args):
......
......@@ -86,7 +86,7 @@ class StringMethods:
urlencode = Method(
name="urlencode",
bash_func="__ct_str_urlencode",
bash_impl='local s="$1" c i len=${#1}; __CT_RET=""; for ((i=0; i<len; i++)); do c="${s:i:1}"; case "$c" in [a-zA-Z0-9.~_-]) __CT_RET+="$c" ;; *) __CT_RET+=$(printf "%%%02X" "\'$c") ;; esac; done; echo "$__CT_RET"',
bash_impl='local s="$1" c i; local LC_ALL=C; local len=${#s}; __CT_RET=""; for ((i=0; i<len; i++)); do c="${s:i:1}"; case "$c" in [a-zA-Z0-9.~_-]) __CT_RET+="$c" ;; *) printf -v c \'%%%02X\' "\'$c"; __CT_RET+="$c" ;; esac; done; echo "$__CT_RET"',
)
concat = Method(
name="concat",
......
......@@ -107,13 +107,18 @@ class Parser:
self.expect (TokenType.AT)
name = self.expect (TokenType.IDENTIFIER, "Expected decorator name").value
obj = None
if self.match (TokenType.DOT):
obj = name
name = self.expect (TokenType.IDENTIFIER, "Expected method name after '.'").value
arguments = []
if self.match (TokenType.LPAREN):
if not self.check (TokenType.RPAREN):
arguments = self.parse_decorator_args ()
self.expect (TokenType.RPAREN, "Expected ')' after decorator arguments")
return Decorator (name=name, arguments=arguments, location=loc)
return Decorator (name=name, arguments=arguments, object=obj, location=loc)
def parse_decorator_args (self) -> List[tuple]:
args = []
......
token = shell.capture ("printenv TELEGRAM_BOT_TOKEN")
if token == "" {
print ("Set TELEGRAM_BOT_TOKEN environment variable")
exit (1)
}
bot = new TelegramBot (token)
@bot.command ("start")
func handle_start (chat_id, text, arg) {
bot.send (chat_id, "Welcome! Commands:\n/help - Show help\n/echo <text> - Echo text")
}
@bot.command ("help")
func handle_help (chat_id, text, arg) {
bot.send (chat_id, "Available commands:\n/start - Start bot\n/help - Show this help\n/echo <text> - Echo back text")
}
@bot.command ("echo")
func handle_echo (chat_id, text, arg) {
if arg == "" {
bot.send (chat_id, "Usage: /echo <text>")
} else {
bot.send (chat_id, arg)
}
}
@bot.on_message ()
func handle_message (chat_id, text, arg) {
bot.send (chat_id, text)
}
bot.run ()
token = shell.capture ("printenv TELEGRAM_BOT_TOKEN")
if token == "" {
print ("Set TELEGRAM_BOT_TOKEN environment variable")
exit (1)
}
base_url = "https://api.telegram.org/bot{token}"
func get_username () {
response = http.get ("{base_url}/getMe")
username = json.get (response, ".result.username")
return username
}
func get_updates (offset) {
url = "{base_url}/getUpdates?timeout=30&offset={offset}"
response = http.get (url)
return response
}
func send_message (chat_id, text) {
encoded = text.urlencode ()
url = "{base_url}/sendMessage?chat_id={chat_id}&text={encoded}"
http.get (url)
}
username = get_username ()
if username == "null" {
print ("Invalid token")
exit (1)
}
print ("Bot @{username} started")
print ("Waiting for messages...")
offset = "0"
while true {
response = get_updates (offset)
count = json.get (response, ".result | length")
if count != "0" {
i = 0
while i < count {
update_id = json.get (response, ".result[{i}].update_id")
chat_id = json.get (response, ".result[{i}].message.chat.id")
text = json.get (response, ".result[{i}].message.text")
first_name = json.get (response, ".result[{i}].message.from.first_name")
offset = update_id + 1
if text != "null" {
print ("{first_name}: {text}")
send_message (chat_id, text)
}
i = i + 1
}
}
}
class TelegramBot {
token = ""
base_url = ""
commands = {}
message_handler = ""
construct (token) {
this.token = token
this.base_url = "https://api.telegram.org/bot{token}"
}
func command (name, handler) {
this.commands.set (name, handler)
}
func on_message (handler) {
this.message_handler = handler
}
func get_me () {
response = http.get ("{this.base_url}/getMe")
return json.get (response, ".result.username")
}
func send (chat_id, text) {
encoded = text.urlencode ()
url = "{this.base_url}/sendMessage?chat_id={chat_id}&text={encoded}"
http.get (url)
}
func run () {
username = this.get_me ()
if username == "null" {
print ("Invalid token")
exit (1)
}
print ("Bot @{username} started")
print ("Waiting for messages...")
offset = "0"
while true {
response = this._get_updates (offset)
count = json.get (response, ".result | length")
if count != "0" {
i = 0
while i < count {
update_id = json.get (response, ".result[{i}].update_id")
chat_id = json.get (response, ".result[{i}].message.chat.id")
text = json.get (response, ".result[{i}].message.text")
from_name = json.get (response, ".result[{i}].message.from.first_name")
offset = update_id + 1
if text != "null" {
print ("{from_name}: {text}")
this._dispatch (chat_id, text)
}
i = i + 1
}
}
}
}
func _get_updates (offset) {
url = "{this.base_url}/getUpdates?timeout=30&offset={offset}"
response = http.get (url)
return response
}
func _dispatch (chat_id, text) {
if text.starts ("/") {
space_idx = text.index (" ")
if space_idx > 0 {
cmd = text.substr (1, space_idx - 1)
arg = text.substr (space_idx + 1, text.len () - space_idx - 1)
} else {
cmd = text.substr (1, text.len () - 1)
arg = ""
}
if this.commands.has (cmd) {
handler = this.commands.get (cmd)
this._invoke (handler, chat_id, text, arg)
return
}
}
if this.message_handler != "" {
this._invoke (this.message_handler, chat_id, text, "")
}
}
func _invoke (handler, chat_id, text, arg) {
handler (chat_id, text, arg)
}
}
......@@ -1279,6 +1279,223 @@ print("Slice len: {count}")
assert "Slice len: 2" in stdout
class TestCallbacks:
"""Tests for callback functions and lambdas as arguments."""
def test_callback_function_parameter(self):
code, stdout, _ = run_ct('''
func apply(callback, value) {
return callback(value)
}
func double(x) {
return x * 2
}
result = apply(double, 5)
print("Result: {result}")
''')
assert code == 0
assert "Result: 10" in stdout
def test_lambda_as_callback(self):
code, stdout, _ = run_ct('''
func apply(callback, value) {
return callback(value)
}
triple = x => x * 3
result = apply(triple, 4)
print("Result: {result}")
''')
assert code == 0
assert "Result: 12" in stdout
def test_inline_lambda_callback(self):
code, stdout, _ = run_ct('''
func apply(callback, value) {
return callback(value)
}
result = apply(x => x + 10, 5)
print("Result: {result}")
''')
assert code == 0
assert "Result: 15" in stdout
def test_callback_with_multiple_args(self):
code, stdout, _ = run_ct('''
func compute(fn, a, b) {
return fn(a, b)
}
add = (x, y) => x + y
result = compute(add, 3, 7)
print("Result: {result}")
''')
assert code == 0
assert "Result: 10" in stdout
def test_callback_in_class_method(self):
code, stdout, _ = run_ct('''
class Processor {
func process(callback, value) {
return callback(value)
}
}
p = new Processor()
square = x => x * x
result = p.process(square, 5)
print("Result: {result}")
''')
assert code == 0
assert "Result: 25" in stdout
def test_callback_chain(self):
code, stdout, _ = run_ct('''
func apply(fn, value) {
return fn(value)
}
double = x => x * 2
add_one = x => x + 1
result = apply(add_one, apply(double, 5))
print("Result: {result}")
''')
assert code == 0
assert "Result: 11" in stdout
class TestUserDecorators:
"""Tests for user-defined decorators (@obj.method() syntax)."""
def test_user_decorator_registers_handler(self):
code, stdout, _ = run_ct('''
class Router {
handlers = {}
func route(path, handler) {
this.handlers.set(path, handler)
}
func dispatch(path) {
if this.handlers.has(path) {
handler = this.handlers.get(path)
handler()
}
}
}
router = new Router()
@router.route("/home")
func home_handler() {
print("Home page")
}
router.dispatch("/home")
''')
assert code == 0
assert "Home page" in stdout
def test_user_decorator_with_callback_args(self):
code, stdout, _ = run_ct('''
class EventBus {
listeners = {}
func on(event, handler) {
this.listeners.set(event, handler)
}
func emit(event, data) {
if this.listeners.has(event) {
handler = this.listeners.get(event)
handler(data)
}
}
}
bus = new EventBus()
@bus.on("click")
func on_click(data) {
print("Clicked: {data}")
}
bus.emit("click", "button1")
''')
assert code == 0
assert "Clicked: button1" in stdout
def test_multiple_user_decorators(self):
code, stdout, _ = run_ct('''
class Commands {
cmds = {}
func register(name, handler) {
this.cmds.set(name, handler)
}
func run(name) {
if this.cmds.has(name) {
handler = this.cmds.get(name)
handler()
}
}
}
cmds = new Commands()
@cmds.register("hello")
func say_hello() {
print("Hello!")
}
@cmds.register("bye")
func say_bye() {
print("Goodbye!")
}
cmds.run("hello")
cmds.run("bye")
''')
assert code == 0
assert "Hello!" in stdout
assert "Goodbye!" in stdout
def test_user_decorator_combined_with_builtin(self):
code, stdout, stderr = run_ct('''
class Commands {
cmds = {}
func register(name, handler) {
this.cmds.set(name, handler)
}
func run(name) {
if this.cmds.has(name) {
handler = this.cmds.get(name)
handler()
}
}
}
cmds = new Commands()
@cmds.register("test")
@log
func test_cmd() {
print("Testing")
}
cmds.run("test")
''')
assert code == 0
assert "Testing" in stdout
class TestClassInstancePassing:
"""Tests for passing class instances to functions and methods."""
......
......@@ -355,3 +355,33 @@ class TestParserDecorators:
decl = ast.statements[0]
assert len(decl.decorators) == 1
assert len(decl.decorators[0].arguments) > 0
def test_user_decorator_simple(self, parse):
ast = parse('@bot.command ("start")\nfunc handler () { }')
decl = ast.statements[0]
assert isinstance(decl, FunctionDecl)
assert len(decl.decorators) == 1
assert decl.decorators[0].object == "bot"
assert decl.decorators[0].name == "command"
def test_user_decorator_no_args(self, parse):
ast = parse("@bot.on_message ()\nfunc handler () { }")
decl = ast.statements[0]
assert decl.decorators[0].object == "bot"
assert decl.decorators[0].name == "on_message"
assert len(decl.decorators[0].arguments) == 0
def test_user_decorator_multiple_args(self, parse):
ast = parse('@router.get ("/api", "json")\nfunc handler () { }')
decl = ast.statements[0]
assert decl.decorators[0].object == "router"
assert decl.decorators[0].name == "get"
assert len(decl.decorators[0].arguments) == 2
def test_mixed_decorators(self, parse):
ast = parse('@bot.command ("start")\n@log\nfunc handler () { }')
decl = ast.statements[0]
assert len(decl.decorators) == 2
assert decl.decorators[0].object == "bot"
assert decl.decorators[1].object is None
assert decl.decorators[1].name == "log"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment