From 59a9d700bdf1d48f6675553c6dd5bf59f37a70b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 3 Jan 2024 15:35:50 -0300 Subject: [PATCH 1/4] #37003: fix test for cython functions in save_session() --- src/sage/misc/session.pyx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/sage/misc/session.pyx b/src/sage/misc/session.pyx index 31454dac993..491f30d00ca 100644 --- a/src/sage/misc/session.pyx +++ b/src/sage/misc/session.pyx @@ -80,7 +80,6 @@ from sage.misc.persist import load, save, loads, dumps state_at_init = None -CythonFunctionType = type(lambda: None) def init(state=None): """ @@ -294,7 +293,7 @@ def save_session(name='sage_session', verbose=False): sage: save_session(tmp_f) sage: save_session(tmp_f, verbose=True) Saving... - Not saving f: f is a function, method, class or type + Not saving f: f is a function or method ... Something similar happens for cython-defined functions:: @@ -302,7 +301,7 @@ def save_session(name='sage_session', verbose=False): sage: g = cython_lambda('double x', 'x*x + 1.5') sage: save_session(tmp_f, verbose=True) Saving... - Not saving g: g is a function, method, class or type + Not saving g: g is a cython function or method ... """ state = caller_locals() @@ -310,11 +309,18 @@ def save_session(name='sage_session', verbose=False): D = {} # We iterate only over the new variables that were defined in this # session, since those are the only ones we will save. - for k in show_identifiers(hidden = True): + for k in show_identifiers(hidden=True): try: x = state[k] - if isinstance(x, (types.FunctionType, types.BuiltinFunctionType, types.BuiltinMethodType, CythonFunctionType, type)): - raise TypeError('{} is a function, method, class or type'.format(k)) + + if isinstance(x, type): + raise TypeError('{} is a class or type'.format(k)) + + if isinstance(x, (types.FunctionType, types.BuiltinFunctionType, types.BuiltinMethodType)): + raise TypeError('{} is a function or method'.format(k)) + + if getattr(type(x), '__name__', None) == 'cython_function_or_method': + raise TypeError('{} is a cython function or method'.format(k)) # We attempt to pickle *and* unpickle every variable to # make *certain* that we can pickled D at the end below. From 927eb7f115cc54ce81b26925845329db1e0fc359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 3 Jan 2024 15:44:50 -0300 Subject: [PATCH 2/4] #37002: fix handling of lazy import variables in save_session() --- src/sage/misc/session.pyx | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/sage/misc/session.pyx b/src/sage/misc/session.pyx index 491f30d00ca..a98016479ff 100644 --- a/src/sage/misc/session.pyx +++ b/src/sage/misc/session.pyx @@ -68,11 +68,12 @@ AUTHOR: import builtins import types -# We want the caller's locals, but locals() is emulated in Cython -cdef caller_locals = builtins.locals - # Sage imports from sage.misc.persist import load, save, loads, dumps +from sage.misc.lazy_import import LazyImport + +# We want the caller's locals, but locals() is emulated in Cython +cdef caller_locals = builtins.locals # This module-scope variables is used to save the # global state of the sage environment at the moment @@ -162,9 +163,13 @@ def _is_new_var(x, v, hidden): # definitely new. if x not in state_at_init: return True + # A lazy import that was there at init time is not new + if isinstance(v, LazyImport): + return False # A variable could also be new even if it was there at init, say if # its value changed. - return x not in state_at_init or state_at_init[x] is not v + return state_at_init[x] is not v + def show_identifiers(hidden=False): r""" @@ -219,17 +224,8 @@ def show_identifiers(hidden=False): sage: show_identifiers(hidden=True) # random output ['__', '_i', '_6', '_4', '_3', '_1', '_ii', '__doc__', '__builtins__', '___', '_9', '__name__', '_', 'a', '_i12', '_i14', 'factor', '__file__', '_hello', '_i13', '_i11', '_i10', '_i15', '_i5', '_13', '_10', '_iii', '_i9', '_i8', '_i7', '_i6', '_i4', '_i3', '_i2', '_i1', '_init_cmdline', '_14'] """ - from sage.doctest.forker import DocTestTask state = caller_locals() - # Ignore extra variables injected into the global namespace by the doctest - # runner - _none = object() - - def _in_extra_globals(name, val): - return val == DocTestTask.extra_globals.get(name, _none) - - return sorted([x for x, v in state.items() if _is_new_var(x, v, hidden) - and not _in_extra_globals(x, v)]) + return sorted([x for x, v in state.items() if _is_new_var(x, v, hidden)]) def save_session(name='sage_session', verbose=False): @@ -303,6 +299,15 @@ def save_session(name='sage_session', verbose=False): Saving... Not saving g: g is a cython function or method ... + + And the same for a lazy import:: + + sage: from sage.misc.lazy_import import LazyImport + sage: lazy_ZZ = LazyImport('sage.rings.integer_ring', 'ZZ') + sage: save_session(tmp_f, verbose=True) + ... + Not saving lazy_ZZ: lazy_ZZ is a lazy import + ... """ state = caller_locals() # This dict D will contain the session -- as a dict -- that we will save to disk. @@ -322,6 +327,9 @@ def save_session(name='sage_session', verbose=False): if getattr(type(x), '__name__', None) == 'cython_function_or_method': raise TypeError('{} is a cython function or method'.format(k)) + if isinstance(x, LazyImport): + raise TypeError('{} is a lazy import'.format(k)) + # We attempt to pickle *and* unpickle every variable to # make *certain* that we can pickled D at the end below. # This seems wasteful, but it guarantees (I hope) that From afaf7d522249fde6666c886c9a2739eba80295c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 3 Jan 2024 15:46:14 -0300 Subject: [PATCH 3/4] Remove extra_globals in class DocTestTask This was introduced to help transition from python 2 to python 3 and is no longer used. --- src/sage/doctest/forker.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index efd28d10abe..56a9fe5e8f6 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -2477,19 +2477,6 @@ class DocTestTask(): ['cputime', 'err', 'failures', 'optionals', 'tests', 'walltime', 'walltime_skips'] """ - extra_globals = {} - """ - Extra objects to place in the global namespace in which tests are run. - Normally this should be empty but there are special cases where it may - be useful. - - For example, in Sage versions 9.1 and earlier, on Python 3 add - ``long`` as an alias for ``int`` so that tests that use the - ``long`` built-in (of which there are many) still pass. We did - this so that the test suite could run on Python 3 while Python 2 - was still the default. - """ - def __init__(self, source): """ Initialization. @@ -2614,10 +2601,6 @@ def _run(self, runner, options, results): # Remove '__package__' item from the globals since it is not # always in the globals in an actual Sage session. dict_all.pop('__package__', None) - - # Add any other special globals for testing purposes only - dict_all.update(self.extra_globals) - sage_namespace = RecordingDict(dict_all) sage_namespace['__name__'] = '__main__' doctests, extras = self.source.create_doctests(sage_namespace) From 2673c8b97afbf557018939cfd01e23746b01bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Tornar=C3=ADa?= Date: Wed, 3 Jan 2024 15:47:38 -0300 Subject: [PATCH 4/4] Don't sort in sage.misc.session.show_identifiers() There's no need to do it. Moreover, dictionaries in python keep insertion order, so we don't need to sort for testing either. Note that in the test sage: a = 10 sage: factor = 20 sage: show_identifiers() ['factor', 'a'] factor is indeed inserted first at sagemath startup (as a function) --- src/sage/misc/session.pyx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sage/misc/session.pyx b/src/sage/misc/session.pyx index a98016479ff..53b732309da 100644 --- a/src/sage/misc/session.pyx +++ b/src/sage/misc/session.pyx @@ -27,7 +27,7 @@ This saves a dictionary with ``w`` as one of the keys:: sage: z = load(os.path.join(d.name, 'session')) sage: list(z) - ['d', 'w'] + ['w', 'd'] sage: z['w'] 2/3 @@ -200,7 +200,7 @@ def show_identifiers(hidden=False): sage: a = 10 sage: factor = 20 sage: show_identifiers() - ['a', 'factor'] + ['factor', 'a'] To get the actual value of a variable from the list, use the :func:`globals()` function.:: @@ -214,7 +214,7 @@ def show_identifiers(hidden=False): sage: _hello = 10 sage: show_identifiers() - ['a', 'factor'] + ['factor', 'a'] sage: '_hello' in show_identifiers(hidden=True) True @@ -222,10 +222,13 @@ def show_identifiers(hidden=False): least in command line mode.:: sage: show_identifiers(hidden=True) # random output - ['__', '_i', '_6', '_4', '_3', '_1', '_ii', '__doc__', '__builtins__', '___', '_9', '__name__', '_', 'a', '_i12', '_i14', 'factor', '__file__', '_hello', '_i13', '_i11', '_i10', '_i15', '_i5', '_13', '_10', '_iii', '_i9', '_i8', '_i7', '_i6', '_i4', '_i3', '_i2', '_i1', '_init_cmdline', '_14'] + ['__builtin__', '_ih', '_oh', '_dh', 'exit', 'quit', '_', '__', '___', + '_i', '_ii', '_iii', '_i1', 'factor', '_i2', '_2', '_i3', 'a', '_i4', + '_i5', '_5', '_i6', '_6', '_i7', '_hello', '_i8', '_8', '_i9', '_9', + '_i10'] """ state = caller_locals() - return sorted([x for x, v in state.items() if _is_new_var(x, v, hidden)]) + return [x for x, v in state.items() if _is_new_var(x, v, hidden)] def save_session(name='sage_session', verbose=False): @@ -288,17 +291,15 @@ def save_session(name='sage_session', verbose=False): sage: f = lambda x : x^2 sage: save_session(tmp_f) sage: save_session(tmp_f, verbose=True) - Saving... - Not saving f: f is a function or method ... + Not saving f: f is a function or method Something similar happens for cython-defined functions:: sage: g = cython_lambda('double x', 'x*x + 1.5') sage: save_session(tmp_f, verbose=True) - Saving... - Not saving g: g is a cython function or method ... + Not saving g: g is a cython function or method And the same for a lazy import:: @@ -307,7 +308,6 @@ def save_session(name='sage_session', verbose=False): sage: save_session(tmp_f, verbose=True) ... Not saving lazy_ZZ: lazy_ZZ is a lazy import - ... """ state = caller_locals() # This dict D will contain the session -- as a dict -- that we will save to disk.