Is there a better/more pythonic way to load an arbitrary set of functions from modules in another folder? - python-3.x

I'm just basically asking:
if it's considered OK to use exec() in this context
if there's a better/more pythonic solution
for any input or comments on how my code could be improved
First, some context. I have main.py which basically takes input and checks to see if I've written a command. Let's say I type '/help'. The slash just tells it my input was supposed to be a command, so then it checks if a function called 'help' exists, and if so, that function will be run.
To keep things tidy in main.py, and to allow myself to add more commands easily, I have a 'commands' directory, with individual command files in it, such as help.py. help.py would look like this for example:
def help():
print("You've been helped")
So then of course, I need to import help() from help.py, which was trivial.
As I added more commands, I decided to add an init.py file where I'd keep all the command import lines of code, and then just do 'from init import *' in main.py. At first, each time I added a command, I'd add another line in init.py to import it. But that wasn't as flexible as I wanted, so I thought, there's got to be a way to just loop through all the .py files in my commands directory and import them. I struggled with this for a while but came up with a solution that works.
In the init.py snippet below, I loop through the commands directory (and a couple others, but they're irrelevant to the question), and you'll see I use the dreaded exec() function to actually import the commands.
loaded, failed = '', ''
for directory in command_directories:
command_list = os.listdir(directory)
command_list.sort()
for command_file in command_list:
if command_file.endswith(".py"):
command_name = command_file.split(".")[0]
try:
# Evil exec() hack to use variable-name directories/modules
# Haven't found a more... pythonic... way to do this
exec(f"from {directory}.{command_name} import {command_name}")
loaded = loaded + f" - Loaded: {command_name}\n"
except:
failed = failed + f" - Failed to load: {command_name}\n"
if debug == True:
for init_debug in [loaded, failed]: print(init_debug)
I use exec() because I don't know a better way to make a variable with the name of the function being loaded, so I use {command_name} in my exec string to arbitrarily evaluate the variable name that will store the function I'm importing. And... well, it works. The functions work perfectly when called from main.py, so I believe they are being imported in the correct namespace.
Obviously, exec() can be dangerous, but I'm not taking any user input into it, just file names. Filenames that I only I am creating. This program isn't being distributed, but if it was, then I believe using exec() would be bad since there's potential someone could exploit it.
If I'm wrong about something, I'd love to hear about it and get suggestions for a better implementation. Python has been very easy to pick up for me, but I'm probably missing some of the fundamentals.
I should note, I'm running python 3.10 on replit (until I move this project to another host).

Related

Running one Autokey script from another Autokey script?

Right now I've got two Autokey scripts (for modularity), one that opens a file, and one that puts text in it.
The one that opens the file has hotkey F1 (and we'll call this script 1 for simplicity), and the one that puts text in it has hotkey F2. I want a new Autokey script, that when I hit F3, it runs both the 1 script and the 2 script.
I've tried making the 3 script just send the F1 and F2 keys, but the timing is all off. It would be better if I could just call 1 and 2 from 3. Is this possible?
Thanks!
https://github.com/autokey/autokey/blob/fc7c6b90f3f28a57ad256829ef2b69e3be5148d2/lib/autokey/scripting.py#L1242
engine.run_script("<description>")
ought to do the trick
"Description" in this context is generally the name of the script in the side bar in the AutoKey interface. If you open up the .json file for the script you can see it for sure, but it will be the name displayed in the side bar unless you have duplicate names for scripts in the same folder or some other edge scenario
AutoKey is not recursive. It does not inspect the output of an AutoKey phrase or script to look for hotkeys or trigger abbreviations which would invoke further actions. That's why your initial solution does not work.
It depends on what you're actually trying to do.
If you have multiple independently useful scripts, the best approach is the one #Icallitvera offers.
If you just want to modularize shared functionality, you can create Python modules of functions and place them in the AutoKey Modules directory. Then, you can import them into any AutoKey script which needs them.
You can find/set the Modules directory from the AutoKey Main Menu via Settings->Configure AutoKey->Script Engine.
At the moment, this approach is limited because scripts invoked this way do not (easily) have access to the AutoKey API, so they can't include any API calls. We plan to fix this in the next major release, AutoKey 0.96. If you really need to do that now, ask on our support list or on Gitter.
I ran into that same problem and the only way I found to work around that restriction is by using the exec() function. Since the scripts with functions to share were already in the AutoKey user folder I used that. So in order to load shared functions of my user module "mygame" into the autokey script I used:
exec(open(engine.configManager.userCodeDir + "/" + "mygame.py").read())
To avoid name collisions when "importing" more than one script and a more module-like feeling I put the functions in classes and instantiate with a variable that is named like the module.
So in the end it looks like this:
mygame.py:
import time
class MyGame:
def GameReload(self):
self.GameExitNoSave()
time.sleep(0.3)
self.GameLoadCurrent()
def GameExitNoSave(self):
keyboard.send_key('d')
time.sleep(0.1)
keyboard.send_key('<up>')
time.sleep(0.05)
keyboard.send_key('<enter>')
def GameLoadCurrent(self):
keyboard.send_key('<down>')
time.sleep(0.1)
keyboard.send_key('<down>')
time.sleep(0.1)
keyboard.send_key('<enter>')
time.sleep(0.5)
keyboard.send_key('<enter>')
mygame = MyGame()
User script in AutoKey:
exec(open(engine.configManager.userCodeDir + "/" + "mygame.py").read())
mygame.GameReload():

Determine if Javascript (NodeJS) code is running in a REPL

I wish to create one NodeJS source file in a Jupyter notebook which is using the IJavascript kernel so that I can quickly debug my code. Once I have it working, I can then use the "Download As..." feature of Jupyter to save the notebook as a NodeJS script file.
I'd like to have the ability to selectively ignore / include code in the notebook source that will not execute when I run the generated NodeJS script file.
I have solved this problem for doing a similar thing for Python Jupyter notebooks because I can determine if the code is running in an interactive session (IPython [REPL]). I accomplished this by using this function in Python:
def is_interactive():
import __main__ as main
return not hasattr(main, '__file__')
(Thanks to Tell if Python is in interactive mode)
Is there a way to do a similar thing for NodeJS?
I don't know if this is the correct way but couldn't find anything else
basically if you
try {
const repl = __dirname
} catch (err) {
//code run if repl
}
it feels a little hacky but works ¯\_(ツ)_/¯
This may not help the OP in all cases, but could help others googling for this question. Sometimes it's enough to know if the script is running interactively or not (REPL and any program that is run from a shell).
In that case, you can check for whether standard output is a TTY:
process.stdout.isTTY
The fastest and most reliable route would just be to query the process arguments. From the NodeJS executable alone, there are two ways to launch the REPL. Either you do something like this without any script following the call to node.
node --experimental-modules ...
Or you force node into the REPL using interactive mode.
node -i ...
The option ending parameter added in v6.11.0 -- will never append arguments into the process.argv array unless it's executing in script mode; via FILE, -p, or -e. Any arguments meant for NodeJS will be filtered into the accompanying process.execArgv variable, so the only thing left in the process.argv array should be process.execPath. Under these circumstances, we can reduce the query to the solution below.
const isREPL = process.execArgv.includes("-i") || process.argv.length === 1;
console.log(isREPL ? "You're in the REPL" : "You're running a script m8");
This isn't the most robust method since any user can otherwise instantiate a REPL from an intiator script which your code could be ran by. For that I'm pretty sure you could use an artificial error to crawl the traceback and look for a REPL entry. Although I haven't the time to implement and ensure that solution at this time.

How to share a variable between 2 pyRevit scripts?

I am using the latest version of pyRevit, v45.
I'm writing some info in temporary files with
myTempFile = script.get_instance_data_file("id")
This creates a file named pyRevit_2018_xxxx_id.tmp in which I store useful info. If I'm not mistaken, the "xxxx" part is changing every time I reload Revit. Now, I need to get access to this information from another pyRevit script.
How can I retrieve the name of the temp file I need to read? In other words, how do I access "myTempFile" from within the second script, which has no idea of the name of "myTempFile"?
I guess I can share somehow that variable between my script, but what's the proper way to do this? I know this must be a very basic programming question, but I'm indeed not a programmer ;)
Thanks a lot,
Arnaud.
Ok, I realise now that my variables in the 1st script cease to exist after its execution.
So for now I wrote the file name in another file, of which I know the name.. That works.
But if there's a cleaner way to do this, I'd be glad to learn ;)
Arnaud
pyrevit.script module provides 4 different methods for creating temporary files based on their use case:
get_instance_data_file:
for data files marked with Revit instance pid. This means that scripts running on another instance will not see this temp file.
http://pyrevit.readthedocs.io/en/latest/pyrevit/script.html#pyrevit.script.get_instance_data_file
get_universal_data_file:
for temp files accessible to all Revit instances and versions
http://pyrevit.readthedocs.io/en/latest/pyrevit/script.html#pyrevit.script.get_universal_data_file
get_data_file:
Base method to get a standard temp file for current revit version
http://pyrevit.readthedocs.io/en/latest/pyrevit/script.html#pyrevit.script.get_data_file
get_document_data_file:
temp file marked with active document (so scripts working on another document will not see this)
http://pyrevit.readthedocs.io/en/latest/pyrevit/script.html#pyrevit.script.get_document_data_file
Each method uses a pattern to create the temp file name. So as long as the call to the method is the same of different scripts, the method generates the same file name.
Example:
Script 1:
from pyrevit import script
tfile = script.get_data_file('mydata')
Script 2:
from pyrevit import script
tempfile = script.get_data_file('mydata')
In this example tempfile = tfile since the file id is the same.
There is documentation on each so make sure you take a look at those and pick the flavor that serves your purpose.

Can I alter Python source code while executing?

What I mean by this is:
I have a program. The end user is currently using it. I submit a new piece of source code and expect it to run as if it were always there?
I can't find an answer that specifically answers the point.
I'd like to be able to say, "extend" or add new features (rather than fix something that's already there on the fly) to the program without requiring a termination of the program (eg. Restart or exit).
Yes, you can definitely do that in python.
Although, it opens a security hole, so be very careful.
You can easily do this by setting up a "loader" class that can collect the source code you want it to use and then call the exec builtin function, just pass some python source code in and it will be evaluated.
Check the package
http://opensourcehacker.com/2011/11/08/sauna-reload-the-most-awesomely-named-python-package-ever/ . It allows to overcome certain raw edges of plain exec. Also it may be worth to check Dynamically reload a class definition in Python

running a .py file, giving it arguments and waiting for its return values

Just like the title says...
Example: I have a file called copy.py. That file wants a path to a file/folder which it will move to another directory and will then return "done" if it successfully moved the file. For some reason I have to run my copy.py file from another python program (it's not given that both files are in the same directory) and wait for copy.py to finish its actions. When it is finished, it should tell me "done" or, let's say, "error", so I know if it actually was successful or not.
Please answer in a way a python beginner can understand...
Often, you can just import the module and call its functionality, but if it's a stand-alone program that expects command-line arguments, etc., then you may want to separate the command-line handling from the functional part of the code, so that you can import and call it as I suggested at the beginning.
Failing that, just treat it like another program:
with os.popen('python copy.py {0} {1}'.format(src, dst)) as copy:
output = copy.readlines()
if 'error' in output:
# Oops...

Resources