# importing .sage files

What's the recommended approach for importing .sage scripts as a module, i.e. into their own namespace? I have a file with ~10k functions and it'd be much nicer to keep them filed tidily away.

I've written a hack sage_import wrapper which seems to work for my use case, but hopefully there's a better way.

edit retag close merge delete

Maybe you can post your hack as an edit to your post?

( 2011-01-13 10:39:12 +0100 )edit

Sort by » oldest newest most voted

You could instead apply the sage preparser, resulting in a .py file which you can then import with no trouble.

If you call "sage filename.sage", sage will preparse and store filename.py in the same directory (as well as carrying out all the commands in the file, which will be problematic in some cases). @ivan-andrus points out that

"sage --preparse filename.sage"

will do just the preparsing :)

Niles

more

I like this, though I don't know if it is answering DSM's question. It would be nice to have something like attach('foo.sage',global_namespace=False) available for those who use it from the command line - if that's what DSM is asking.

( 2011-01-13 13:34:08 +0100 )edit

I haven't seen any problems with just running sage --preparse FILE.sage. What problems do you have?

( 2011-01-15 09:03:57 +0100 )edit

Ah *that's* the command I was looking for! sage-preparse is a separate executable which needs to be called in the right way, which I couldn't figure out; passing the --preparse option to sage seems to do the trick. Thanks!

( 2011-01-15 10:27:47 +0100 )edit
1

Well, invoking the functions in sage.misc.preparser is what my hack currently does. One problem with preparser approaches -- mine included, at least so far -- is that the resulting code shows up in a mymodule.somefunc?? call in its postprocessed form, which is often hard to read. (Enough __sage_const_ bits floating around and even simple formulae look confusing.) This gives writing Sage code _in Sage code_ a weirdly second-class status. I'm leaning towards modifying the .func_code.co_* data so that I can see what I actually wrote [possibly putting the post-processed version below it]; this is a little tricky, though.

( 2011-01-15 11:13:59 +0100 )edit

ah; that is annoying

( 2011-01-15 11:22:58 +0100 )edit

Just to make @niles' answer more explicit: You can add the following function to the beginning of your main sage script.

## Hack to import my own sage scripts
def my_import(module_name, func_name='*'):
import os
os.system('sage --preparse ' + module_name + '.sage')
os.system('mv ' + module_name + '.sage.py ' + module_name + '.py')

from sage.misc.python import Python
python = Python()
python.eval('from ' + module_name + ' import ' + func_name, globals())

Then, to import all functions from my_lib.sage, you can do

my_import("my_lib")  # from my_lib import *

To import a particular function, say my_func, you can do

my_import("my_lib", "my_func") # from my_lib import my_func

(I created this function using this solution)

more

You can access the Sage preparser as a function call--no need to use os.system for any of this. I'll post an answer with a simplified solution.

( 2019-12-05 12:12:41 +0100 )edit

Note that the object returned by globals() in different contexts might be distinct (I was surprised to find that by comparing them). I had to add a parameter globals_used (I made it default to globals()) which is assigned by globals() upon calling like so: my_import("my_lib", "my_func", globals()). This globals_used parameter is passed as the argument for the eval at the end of the function like so: python.eval('from ' + module_name + ' import ' + func_name, globals_used). This way the imported functions are actually added to the correct globals() object and thus are available to use in the context in which the import was called.

( 2021-07-07 14:42:23 +0100 )edit

Here's a rough sketch of a sage_import that works similarly to the Python import statement on .sage files, including searching sys.path for the file (which by default includes your current working directory), but this part is optional I suppose.

# sage_import.py
import imp
import inspect
import os
import sys

import sage.all

def sage_import(modname, fromlist=None, namespace=None):
"""
Import a .sage module from the filename <modname>.sage

Returns the resulting Python module.  If fromlist is given, returns
just those members of the module into the global namespace where the
function was called, or the given namespace.
"""

filename = modname + '.sage'

for path in sys.path:
modpath = os.path.join(path, filename)
if os.path.isfile(modpath):
break
else:
raise ImportError('no file {} on sys.path'.format(filename))

with open(modpath) as fobj:
mod = imp.new_module(modname)
mod.__file__ = modpath
# Fill with all the default Sage globals
# We could just do a dict.update but we want to exclude dunder
# and private attributes I guess
for k, v in sage.all.__dict__.items():
if not k.startswith('_'):
mod.__dict__[k] = v

exec code in mod.__dict__

if namespace is None:
namespace = inspect.currentframe().f_back.f_globals

if fromlist is not None:
# First check that each name in fromlist exists before adding
# any of them to the given namespace.
for name in fromlist:
if name not in mod.__dict__:
raise ImportError('cannot import name {!r} from {}'.format(
name, filename))

for name in fromlist:
namespace[name] = mod.__dict__[name]
else:
namespace[modname] = mod

Given a file in my current directory named a.sage containing:

# a.sage
a = 1 / 2

I can use this either in Sage or in a standard Python interpreter like

>>> from sage_import import sage_import
>>> sage_import('a', fromlist=['a'])
>>> a
1/2
>>> type(a)
<type 'sage.rings.rational.Rational'>

This version works for Python 2. It can be adapted to Python 3 with some small tweaks.

There is an open issue to build this functionality directly into Sage so that you can use the standard import statement to import .sage modules. This can be done but it requires a bit of care. I think a prototype was started at one point but it still hasn't been submitted.

more

See a somewhat related comment in the ticket.

( 2019-12-05 17:23:15 +0100 )edit