Summary
We explain how to create a basic import hook.
We show how to do some simple source modification using
token_utilsWe show how to use the
-sor--showcommand line flag to get some debugging information.
Create your first import hook: the basics
You’ve seen how to use ideas import hooks; now it is time to
create your first one. We will use our "Hello world" example,
which uses function as equivalent to lambda.
How to do this
Suppose you had access to the source of a program using
function as a keyword instead of lambda.
Perhaps something like the following:
"""greet_script.py
This is a test demonstrating the use of our hook to replace
function by lambda."""
greet = function name: print(f"Hello {name}!")
greet("World")
So that you could write:
>>> import greet_script
Hello World!
If you had access to that source, all you would need to do is:
modified_source = source.replace("function", "lambda")
and have Python execute modified_source instead of the original source.
Here’s how we can do it using ideas:
# simple_replacement.py
from ideas import import_hook
def some_arbitrary_name(source, **kwargs):
return source.replace("function", "lambda")
import_hook.create_hook(
transform_source=some_arbitrary_name,
name=__name__ # required
)
That’s it! Let’s try it out.
> py
Python 3.11.9 ...
>>> import greet_script
Hello World!
It works! … almost … Let’s find out more:
>>> help(greet_script)
Help on module greet_script:
NAME
greet_script - greet_script.py
DESCRIPTION
This is a test demonstrating the use of our hook to replace
lambda by lambda.
FUNCTIONS
greet lambda name
FILE
c:\\users\\andre\\github\\ideas\\docs_examples\\first_import_hook\\greet_script.py
Something is not ideal with the DESCRIPTION: it says that it replaces
lambda by lambda, which is not very helpful.
Still, as a first step, it shows how easy it is to write an
import hook that modifies the source code using ideas.
Instead of importing everything within a standard Python
interpreter, let’s execute the code using ideas on the command line.
> ideas -a simple_replacement greet_script
Module <module 'simple_replacement' from 'C:\\Users\\Andre\\github\\ideas\\docs_examples\\first_import_hook\\simple_replacement.py'> does not contain a function named add_hook
Hello World!
Something is not quite right: using the -a flag told ideas to import
the module (which it did), then find the add_hook function and execute it.
However, our example did not have such a function defined, as it directly
created an import hook which was then used to replace function by
lambda when the file greet_script.py was executed.
So, in spite of the error message from ideas,
everything worked the way we wanted.
Actual code
Here’s the content of the similar import hook included in ideas when you install it on your computer.
"""This module enables someone to use ``function`` as a keyword
equivalent to ``lambda``.
"""
from ideas import create_hook
import token_utils
def transform_source(source, **_kwargs):
"""This performs a simple replacement of ``function`` by ``lambda``."""
new_tokens = []
for token in token_utils.tokenize(source):
# token_utils allows us to easily replace the string content
# of any token
if token == "function":
token.string = "lambda"
new_tokens.append(token)
return token_utils.untokenize(new_tokens)
def add_hook(**_kwargs):
"""Creates and automatically adds the import hook in sys.meta_path"""
hook = create_hook(transform_source=transform_source, name=__name__)
return hook
add_hook
Rather than inserting our import hook immediately upon execution
of this module, we put the code to do so in the function
add_hook, and return the hook that was created.
This has at least four benefits:
We can control when the hook is created.
We can use the return value to change some of its parameters, for example to temporarily disable it, or to remove it altogether. This can be particularly useful for testing.
We can optionally add arguments to
add_hook; we will do so in more complex examplesPerhaps the most import benefit is that, as we have seen before, we can invoke ideas from the command line with the
-aor--add_hookflag,ideas --add_hook function_keywordwhich imports
function_keywordand callsfunction_keyword.add_hook().
Using token_utils
To replace function by lambda only when it is meant to be
used as a keyword, we break up the code in a series of tokens
and only replace function by lambda when it occurs as
an individual token. Rather than using directly the tokenizer
from Python’s standard library, we use our own version which has some useful
added features. For example, in almost all cases, the relevant
characteristic of a token is its string representation.
We can compare a token directly to a string like we did in the code above on line 16.
Note that, just like:
def lambda():
pass
would raise a SyntaxError, the same would occur with:
def function():
pass
using our import hook.
Once we’re done with replacing all function tokens by lambda,
we convert the tokens back into a string by calling our
utility function untokenize on line 19.
Finally, by convention, we use the
same name, transform_source that is used as a keyword
argument for import_hook.create_hook;
unlike add_hook, using the specific name transform_source
is not required by ideas.
Debugging help
You can use the -s (or --show_changes) flag to find out
what changes have been made by the source transformation to the original script.
> ideas -a function_keyword my_program -s -i
#========== Original source from docs_examples/usage/my_program.py ====
# flake8: noqa
# my_program.py
square = function x: x**2
print(f"{square(4)} is the square of 4.")
if __name__ == '__main__':
print(f"And the square of 5 is {square(5)}")
#=== End of Original source from docs_examples/usage/my_program.py ====
#========== Transformed source ====
# flake8: noqa
# my_program.py
square = lambda x: x**2
print(f"{square(4)} is the square of 4.")
if __name__ == '__main__':
print(f"And the square of 5 is {square(5)}")
#=== End of Transformed source ====
16 is the square of 4.
And the square of 5 is 25
Ideas Console version 0.2.1. [Python version: 3.11.9]
ideas>
When using import hooks with a standard Python interpreter, only modules that are imported are affected as they are imported. Code that is entered later in the interpreter is left unchanged. However, this is not the case for the ideas console. In the ideas console, since we see the original source (the code we wrote), only the changed source is shown.
ideas> cube = function x: x**3
New: cube = lambda x: x**3
Inside the ideas console, you can turn on or off this feature as follows:
ideas> ideas_state.show_changes = False
ideas> double = function x: 2*x
ideas> ideas_state.show_changes = True
ideas> triple = function x: 3*x
New: triple = lambda x: 3*x
ideas>
Tip
Try out ideas_state.help() to get an idea of what
information might be available or modified via ideas_state.
Complete argument list for transform_source
In the above example, we had some unspecified keywords arguments
passed to transform_source.
The last time this documentation was updated, the list of possible
arguments could be found in the arguments of hook.transform_source
below.
def source_transforms(
self,
source,
filename=None,
module=None,
callback_params=None,
console_dict=None,
**kwargs,
):
"""Applies a source transformation from the installed import hooks.
Returns a new source.
"""
if kwargs:
print(
"FatalError: unkown argument in session.State.source_transform:", kwargs
)
print("This argument cannot be handled correctly.")
print("Ideas's code needs to be adapted to make use of this argument.")
print("For now, it will be ignored.")
for hook in self._hooks:
if hook.enabled and hook.transform_source is not None:
source = hook.transform_source(
source,
filename=filename,
module=module,
callback_params=callback_params,
console_dict=console_dict,
)
return source
filename can sometimes be the name of the ideas console.
When using IPython or Jupyter, only the source is passed back to transform_source.