import os
async def cmdrun(client, message, prefix):
cmd = message.content.split(' ')[0].split(prefix)[1]
args = message.content.split(cmd)[1][1:].split(' ')
for filename in os.listdir('./commands'):
if filename.endswith('.py'):
imported = filename.split('.py')[0]
strin = f"from commands.{imported} import name, aliases, run\nx = name()\ny = aliases()\nawait message.channel.send(x + y)\nif x == {cmd} or {cmd} in y:\n await run(client, message, args)"
exec(strin)
I am making a discord bot with discord.py.
What is the error
That error is definitely always associated with a missing double-inverted/single-inverted comma. Here's my suggestions:
Try running the prepared lines of code you're attempting to use as is.
I couldn't find anything in the new documentation, but the old
documentation suggest that ...in the current implementation, multi-line compound statements must end with a newline: exec "for v in seq:\n\tprint v\n" works, but exec "for v in seq:\n\tprint v" fails with SyntaxError. Perhaps try adding a new line character at the end?
Try using triple inverted commas.
Hope this solves the issue!
Related
I tried executing
import ast
ast.literal_eval('5+5')
Then I got ValueError: malformed node or string on line 1: <ast.BinOp object at 0x70e6bd2830>.
So then I tried another:
import ast
ast.literal_eval(str(5+5))
And it evaluates it successfully:
10
But when I finally used it on an actual command $math 5+5
#client.command()
async def math(ctx, expression):
resp = ast.literal_eval(str(expression))
await ctx.send(resp)
I still get the same error: ValueError: malformed node or string on line 1: <ast.BinOp object at 0x7d05357700> for some reason. I'm using Python 3.10.5.
How can I solve this? Any help would be appreciated.
Whole code:
import discord, ast
from discord.ext import commands
TOKEN = ""
client = commands.Bot(command_prefix="$")
prefix = client.command_prefix
def init():
client.run(TOKEN, reconnect=False)
#client.event
async def on_connect():
print('Connecting to server...')
#client.event
async def on_ready():
await client.change_presence(status=discord.Status.idle)
print('Logged in as ' + str(client.user))
print('Prefix: ' + str(prefix))
#client.command()
async def math(ctx, expression):
resp = ast.literal_eval(str(expression))
await ctx.send(resp)
init()
You're no longer allowed to do addition in literal_eval since python 3.7.
I found some "safe expressions" to work with from this answer. Here's a simple demo:
import ast
value = input()
co = ast.parse(value, mode='eval')
for node in ast.walk(co):
if not isinstance(node, (ast.Expression, ast.Constant, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow, ast.BinOp, ast.USub, ast.UAdd, ast.UnaryOp)):
raise RuntimeError(f'Sorry! You gave an illegal syntax node: {node.__class__.__name__}')
print(eval(value))
This successfully evaluates valid math in python, such as 5 + 7*8 = 61 or 5 ** 7 = 78125, and of course 0.1 + 0.2 = 0.30000000000000004.
Any bad operations that can potentially cause problems, such as Call or Name (meaning variable_names_like_this and function()) are not allowed. If you find any allowed operations that I missed, you can simply add them to the tuple inside the isinstance to allow them. You can precisely customize which syntax operations are allowed or not.
As a fair warning to anyone who is tempted to use eval without restricting the syntax you can use, this can execute regardless of your namespace fencing. It gets the object class, from which it gets a module to get __builtins__, after which getting __import__ and doing bad things with that. For more danger, replace 'echo hi' here with 'rm -rf /':
[v for c,v in [c for c in ''.__class__.__bases__[0].__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__.items() if c == '__import__'][0]('os').system('echo hi')
The error you are getting appears to be because, according to the documentation:
The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, None and Ellipsis.
So when you do:
ast.literal_eval('5+5')
The problem is + which is a BinOp (which corresponds to the <ast.BinOp object at 0x7d05357700> part of the error message) and cannot be used.
However, when you do:
ast.literal_eval(str(5+5))
The order of evaluation is as follows:
ast.literal_eval(str(5+5)) -> ast.literal_eval(str(10)) -> ast.literal_eval('10')
Which no longer has + involved.
How do I go about retrieving a list of commands (Commands) that require the user to have the 'administrator' permission (to use them)? I am making a 'help' command; I would like to be able to retrieve all commands that require a certain permission in a list.
For example:
class Moderate(commands.Cog):
#commands.command()
#has_permissions(administrator=True)
async def kick(ctx, member):
#kick member
cog = Moderate
print(cog.commands.checks) -> {"kick" : administrator=True} #How to get this or the commands my bot has with this permission requirement?
My solution to this problem is to read the file and get the commands' names from it by checking if there's a "has_permissions(" string before it:
import os
file_path = os.path.abspath(__file__) #Get the file path of this file
self = open(file_path, "r", encoding="utf-8") #Specify the encoding as it's a Python file
code = self.readlines()
commands = [] #This is the list of all the commands your bot has
for line_number, line in enumerate(code):
if "has_permissions(administrator" in line: #Check if the decorator ("#has_permissions(administrator)") is in the line
command = code[line_number + 1] #Get the line after the decorator's
command_name = command.replace("async def ", "").strip()[:-1] #Removing "async def ", colon and extra spaces
if "command = code[line_number + " not in command_name: #Adding all commands' names except this code we're using
commands.append(command_name)
print(commands) #See the 100 commands your bot has
This will not work if you check if the user has the 'administrator' permission in a way other than the #has_permissions(administrator=True) decorator. For example, if you have a 'ban' command like this, it won't be added to the list:
bot.command()
async def ban(message, member:discord.Member):
if message.author.guild_permissions.administrator: #The method won't work if you check using this
#Ban the member
This also won't work if you have extra blank lines between the decorator and the command's definition etc.
However, you can modify this to meet your requirements.
Here's a less hacky way of doing it that I personally prefer:
We can use a decorator to set a special attribute in the Command object, which we can then look at later. (Note: it is important that the #special decorator is placed before the #client.command. It needs to work off the Command object, not the actual callable function itself.)
def special(command):
command.__special_flag__ = None # the value is irrelevant, you just need to set it
return command
#special
#client.command()
async def special_command(ctx):
await ctx.send('hi')
#special
#client.command()
async def list_special_commands(ctx):
for command in client.commands:
if hasattr(command, '__special_flag__'): # this checks if "__special_flag__" is set
await ctx.send(f'special command! {command}')
await ctx.send('ok')
You can then iterate through every command in the client and check if that flag is set. You can then add this extra decorator to any command you want to check permissions for. Then, in your help command, only display commands with this flag set.
Output looks like this:
As #The Amateur Coder mentioned, this might take a bit of time to add them all, so you can make this combined decorator that does all of it:
def combine_stuff(function):
function = commands.has_permissions(...)(function)
command = client.command(...)(function)
command = special(command)
Explanation
There exists a very simple way to see which commands require administrator permissions. All command checks' predicates are in closures, and for has_permissions, one of the nonlocal variables of these functions will be the specified permissions.
This code must be run after all the commands are defined. For example, it can be run in on_ready, or before bot.run. You do not need to modify any other functions to run this code.
Code
#bot.event
async def on_ready():
command_list = []
for command in bot.walk_commands():
for check in command.checks:
closure = check.__closure__
if closure is None:
continue
# loop through variables used by the check
for cell in closure:
# if administrator=True was passed
if cell.cell_contents.get("administrator"):
command_list.append(command)
break
print(command_list)
Reference
How do I access/modify variables from a function's closure?
Definition of has_permissions
I am trying to mix Elixir with Python using ErlPort, so I decided to read some tutorials about it and the documentation related about everything involved. I understand how works the logic and what does each function. However, I am having problems casting a message and receiving the Python response.
Based on what I read and what I have done I understand that when I cast a message with cast_count/1, this is handle by handle_cast/2 and then is handle by the Python function handle_message() and then this one cast the message with the function cast_message() and the imported one cast()from erlport.erlang. Finally, Elixir should handle the message received from Python with handle_info/2. I think this function is not being executed but I don't know the reason, although I have investigated a lot this stuff in different sources and in the documentation of GenServer and ErlPort.
In my case I have the next structure: lib/python_helper.ex to make ErlPort works and lib/server.ex to call and cast the Python functions.
lib/python_helper.ex
defmodule WikiElixirTest.PythonHelper do
def start_instance do
path =
[:code.priv_dir(:wiki_elixir_test), "python"]
|> Path.join()
|> to_charlist()
{:ok, pid} = :python.start([{:python_path, path}])
pid
end
def call(pid, module, function, arguments \\ []) do
pid
|> :python.call(module, function, arguments)
end
def cast(pid, message) do
pid
|> :python.cast(message)
end
def stop_instance(pid) do
pid
|> :python.stop()
end
end
lib/server.ex
defmodule WikiElixirTest.Server do
use GenServer
alias WikiElixirTest.PythonHelper
def start_link() do
GenServer.start_link(__MODULE__, [])
end
def init(_args) do
session = PythonHelper.start_instance()
PythonHelper.call(session, :counter, :register_handler, [self()])
{:ok, session}
end
def cast_count(count) do
{:ok, pid} = start_link()
GenServer.cast(pid, {:count, count})
end
def call_count(count) do
{:ok, pid} = start_link()
GenServer.call(pid, {:count, count}, :infinity)
end
def handle_call({:count, count}, _from, session) do
result = PythonHelper.call(session, :counter, :counter, [count])
{:reply, result, session}
end
def handle_cast({:count, count}, session) do
PythonHelper.cast(session, count)
{:noreply, session}
end
def handle_info({:python, message}, session) do
IO.puts("Received message from Python: #{inspect(message)}")
{:stop, :normal, session}
end
def terminate(_reason, session) do
PythonHelper.stop_instance(session)
:ok
end
end
priv/python/counter.py
import time
import sys
from erlport.erlang import set_message_handler, cast
from erlport.erlterms import Atom
message_handler = None
def cast_message(pid, message):
cast(pid, (Atom('python', message)))
def register_handler(pid):
global message_handler
message_handler = pid
def handle_message(count):
try:
print('Received message from Elixir')
print(f'Count: {count}')
result = counter(count)
if message_handler:
cast_message(message_handler, result)
except Exception as e:
print(e)
pass
def counter(count=100):
i = 0
data = []
while i < count:
time.sleep(1)
data.append(i+1)
i = i + 1
return data
set_message_handler(handle_message)
Note: I removed #doc to light the code snippets. And yes, I know sys isn't being used at this moment and that catch Exception in Python try block is not the best approach, it is just temporal.
If I test it in iex (iex -S mix), I get the next:
iex(1)> WikiElixirTest.Server.cast_count(19)
Received message from Elixir
Count: 19
:ok
I want to note that call_count/1 and handle_call/1 works fine:
iex(3)> WikiElixirTest.Server.call_count(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
What I am doing wrong that the communication of Elixir with Python is successful but not the communication of Python with Elixir when I cast a message?
Well, #Everett reported in the question that I closed wrong the atom in counter.py.
def cast_message(pid, message):
cast(pid, (Atom('python', message)))
This must be:
def cast_message(pid, message):
cast(pid, (Atom('python'), message))
Although this doesn't solve the problem, it helps me to get with the solution. After fixed the first part ((Atom('python'), message)), when I execute cast_count/1 I get a message from Python:
iex(1)> WikiElixirTest.Server.cast_count(2)
Received message from Elixir
Count: 2
:ok
bytes object expected
Although it confuses me a bit at the first time because I awaited something like "Received message from Python: bytes object expected", due handle_info/2. However, I decided to inspect the code of ErlPort and I found the error in the line 66 of erlterms.py, part of the Atom class. Thus the error was in the first part of the message, the atom, so I specified it as binary:
def cast_message(pid, message):
cast(pid, (Atom(b'python', message)))
Then I checked it:
iex(2)> WikiElixirTest.Server.cast_count(2)
:ok
Received message from Elixir
Count: 2
Received message from Python: [[1, 2]]
And it works nicely! So the problem there, even taking into account my mistyping of the message tuple, was that Atom() needs a binary.
Probably this misunderstanding may be due the first ErlPort tutorial I followed uses Python 2, version in which str is a string of bytes and in Python 3 it would be necessary to convert the string to bytes, due str is a string of text.
I am trying to learn how to use Python-click. I was not able to use a help parameter with one of my options so I finally gave up and changed the code to not include help for that option. However, despite closing and restarting Python and now rebooting my computer the error message associated with trying to use the help parameter is still appearing.
Code:
import click
def something():
pass
#click.command()
#click.argument('dest_dir',type=click.Path(exists=True, readable=True,
resolve_path=True, dir_okay=True),
help='Location of directory where results will be saved')
#click.option('--use_terms', is_flag=True,
help='Process strings based on terms or phrases')
#click.option('--use_query', is_flag=True, help='Process string based on
search query')
#click.option('--search_phrase', '-s', multiple=True)
def do_process(dest_dir,use_terms,use_query,*search_phrase):
""" testing setting parameters for snip tables"""
outref = open('e:\\myTemp\\testq.txt')
ms = dest_dir + '\n'
if use_terms:
ms += use_term + '\n'
else:
ms += use_query + '\n'
for each in search_phrase:
x = something()
ms += each + '\n'
outref.writelines(ms)
outref.close()
if __name__ == "__main__":
do_process()
Originally for the last #click.option I had
#click.option('--search_phrase', '-s', multiple=True, help='The search phrase to use')
I kept getting an error message that I could not solve relating to having an unknown parameter help. I ditched it, changed to what is above and now I am getting a similar error,
I then shut down Python, I closed my module and then restarted Python opened and ran my code again and still getting this error message
Traceback:
Traceback (most recent call last):
File "C:\Program Files\PYTHON\snipTables\test_snip_click.py", line 14, in <module>
#click.option('--search_phrase', '-s', multiple=True)
File "C:\Program Files\PYTHON\lib\site-packages\click\decorators.py", line 148, in decorator
_param_memo(f, ArgumentClass(param_decls, **attrs))
File "C:\Program Files\PYTHON\lib\site-packages\click\core.py", line 1618, in __init__
Parameter.__init__(self, param_decls, required=required, **attrs)
TypeError: __init__() got an unexpected keyword argument 'help'
So then I shut down Python Idle, I saved and closed my code and then restarted Python, reopened my code, but I am still getting the same traceback except notice that the traceback has the line of code I switched to after beating my head hard against the monitor and giving up
I am getting ready to reboot but am really curious as to the cause.
I rebooted and still am getting the same error
Renaming the file and running again did not change outcome - same traceback
The problem is that click does not accept a help string with an argument parameter. It is interesting behavior. The help string associated with the argument will be the string in the function that processes the argument and options.
The error message will always show up associated with the last option. So the correct code for this example would be
import click
def something():
pass
#click.command()
#click.argument('dest_dir',type=click.Path(exists=True, readable=True,
resolve_path=True, dir_okay=True)
##Notice no help string here
#click.option('--use_terms', is_flag=True,
help='Process strings based on terms or phrases')
#click.option('--use_query', is_flag=True, help='Process string based on
search query')
#click.option('--search_phrase', '-s', multiple=True)
def do_process(dest_dir,use_terms,use_query,*search_phrase):
""" testing setting parameters for snip tables"""
outref = open('e:\\myTemp\\testq.txt')
ms = dest_dir + '\n'
if use_terms:
ms += use_term + '\n'
else:
ms += use_query + '\n'
for each in search_phrase:
x = something()
ms += each + '\n'
outref.writelines(ms)
outref.close()
if __name__ == "__main__":
do_process()
This runs fine the problem I was originally having is that click was not doing a good job of explaining the source of the error. Above, even though I got rid of the help string in the option the click parser associates the help string from the argument with the last option it parses.
Maybe you renamed the source file and you are running an old version that was compiled with the previous name?
try deleting *.pyc files
I'm trying to parse some code with AST, but I'm having an issue because of backslash continuation character.
When I have a continuation character \, textwrap will not manage to dedent the code, I would like to know how to get rid of it.
code = """
def foo():
message = "This is a very long message that will probably need to wrap at the end of the line!\n \
And it actually did!"
"""
import textwrap
print textwrap.dedent(code)
import ast
ast.parse(textwrap.dedent(code))
I'm adding more details to clarify the question:
I have a module nemo.py with the following content:
class Foo(object):
def bar(self):
message = "This is a very long message that will probably need to wrap at the end of the line!\n \
And it actually did!"
and the main module trying to parse the code:
import ast
import nemo
import inspect
import textwrap
code = str().join(inspect.getsourcelines(nemo.Foo.bar)[0])
ast.parse(textwrap.dedent(code))
And the traceback:
Traceback (most recent call last):
File "/Users/kelsolaar/Documents/Development/Research/_BI.py", line 7, in <module>
ast.parse(textwrap.dedent(code))
File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/ast.py", line 37, in parse
return compile(source, filename, mode, PyCF_ONLY_AST)
File "<unknown>", line 1
def bar(self):
^
IndentationError: unexpected indent
This is because you misunderstood what textwrap.dedent() does.
It only remove any common leading white spaces. In your case there's no common leading white space, therefore nothing is removed.
Moreover, what you want is actually \\ instead of \n \ in this case. This is because you actually want what is printed to be parsed. \\ will print only one \ and it's what you want. \n \ will print a new line within "..." clause which is invalid.
Now consider this code:
>>> code = """
def foo():
message = "This is a very long message that will probably need to wrap at the end of the line! \\
And it actually did!"
"""
>>> print textwrap.dedent(code)
def foo():
message = "This is a very long message that will probably need to wrap at the e
nd of the line! \
And it actually did!"
>>> ast.parse(textwrap.dedent(code))
<_ast.Module object at 0x10e9e5bd0>
In this case there is common leading white spaces, and hence they are removed.
Edit:
If you want to get rid of the \ all together, you can consider using """My sentence""" for message in def bar.
For the second part of the question I the following simple replace covers my needs: code.replace("\\n", str())
import ast
import nemo
import inspect
import textwrap
code = str().join(inspect.getsourcelines(nemo.Foo.bar)[0])
code.replace("\\\n", str())
ast.parse(textwrap.dedent(code))