From 3d8d1805f33193e660e90da2f9a8da603ceb859f Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Sun, 5 Jan 2025 01:19:41 +0800 Subject: [PATCH 1/3] Remove `--fiximports` helper It did its job; the remaining imports are okay (or would need manual work anyway). This is part of trying to eliminate most in `misc`. --- src/.relint.yml | 7 +- src/bin/sage | 9 - .../en/developer/packaging_sage_library.rst | 2 - src/doc/en/reference/misc/index.rst | 1 - src/sage/misc/dev_tools.py | 738 ------------------ src/sage/misc/meson.build | 2 - src/sage/misc/replace_dot_all.py | 470 ----------- 7 files changed, 2 insertions(+), 1227 deletions(-) delete mode 100644 src/sage/misc/dev_tools.py delete mode 100644 src/sage/misc/replace_dot_all.py diff --git a/src/.relint.yml b/src/.relint.yml index 80dcb8600c5..ef60d44ba22 100644 --- a/src/.relint.yml +++ b/src/.relint.yml @@ -42,13 +42,10 @@ - name: 'namespace_pkg_all_import: import from .all of a namespace package' hint: | - Sage library code should not import from sage.PAC.KAGE.all when sage.PAC.KAGE is an implicit - namespace package. Type import_statements("SOME_IDENTIFIER") to find a more specific import, - or use 'sage --fiximports' to fix automatically in the source file. - # Keep in sync with SAGE_ROOT/src/sage/misc/replace_dot_all.py + Sage library code should not import from sage.PAC.KAGE.all. pattern: 'from\s+sage(|[.](arith|categories|combinat|crypto|databases|data_structures|dynamics|ext|game_theory|games|geometry|graphs|groups|interfaces|manifolds|matrix|matroids|misc|modules|monoids|numerical|probability|quadratic_forms|quivers|rings|sat|schemes|sets|stats|symbolic|tensor)[a-z0-9_.]*|[.]libs)[.]all\s+import' # imports from .all are allowed in all.py; also allow in some modules that need sage.all - filePattern: '(.*/|)(?!(all|benchmark|dev_tools|parsing|sage_eval|explain_pickle|.*_test))[^/.]*[.](py|pyx|pxi)$' + filePattern: '(.*/|)(?!(all|benchmark|parsing|sage_eval|explain_pickle|.*_test))[^/.]*[.](py|pyx|pxi)$' - name: 'namespace_pkg_all_import_2: Module-level import of .all of a namespace package' hint: | diff --git a/src/bin/sage b/src/bin/sage index d102056b6c6..276e8fbe324 100755 --- a/src/bin/sage +++ b/src/bin/sage @@ -488,10 +488,6 @@ usage_advanced() { echo " --fixdoctests file.py" echo " -- Run doctests and replace output of failing doctests" echo " with actual output." - echo " --fiximports " - echo " -- Replace imports from sage.PAC.KAGE.all by specific" - echo " imports when sage.PAC.KAGE is an implicit namespace" - echo " package" echo " --fixdistributions " echo " -- Check or update '# sage_setup: distribution'" echo " directives in source files" @@ -955,11 +951,6 @@ if [ "$1" = '-startuptime' -o "$1" = '--startuptime' ]; then exec sage-startuptime.py "$@" fi -if [ "$1" = '-fiximports' -o "$1" = '--fiximports' ]; then - shift - exec sage-python -m sage.misc.replace_dot_all "$@" -fi - if [ "$1" = '-fixdistributions' -o "$1" = '--fixdistributions' ]; then shift exec sage-python -m sage.misc.package_dir "$@" diff --git a/src/doc/en/developer/packaging_sage_library.rst b/src/doc/en/developer/packaging_sage_library.rst index 9fbb4271727..cf1521940ae 100644 --- a/src/doc/en/developer/packaging_sage_library.rst +++ b/src/doc/en/developer/packaging_sage_library.rst @@ -300,8 +300,6 @@ distribution -- which then must be declared as a run-time dependency. :mod:`sage.rings.all`. To audit the Sage library for such imports, use ``sage --tox -e relint``. - In most cases, the imports can be fixed automatically using the - tool ``sage --fiximports``. - Replace module-level imports by method-level imports. Note that this comes with a small runtime overhead, which can become diff --git a/src/doc/en/reference/misc/index.rst b/src/doc/en/reference/misc/index.rst index bdc30927a55..69d82493f72 100644 --- a/src/doc/en/reference/misc/index.rst +++ b/src/doc/en/reference/misc/index.rst @@ -265,7 +265,6 @@ Miscellaneous Inspection and Development Tools sage/misc/sageinspect sage/misc/edit_module sage/misc/classgraph - sage/misc/dev_tools sage/misc/function_mangling sage/misc/rest_index_of_methods diff --git a/src/sage/misc/dev_tools.py b/src/sage/misc/dev_tools.py deleted file mode 100644 index b11b2078129..00000000000 --- a/src/sage/misc/dev_tools.py +++ /dev/null @@ -1,738 +0,0 @@ -r""" -Some tools for developers - -AUTHORS: - -- Nicolas M. Thiery: initial version - -- Vincent Delecroix (2012 and 2013): improve import_statements -""" -# **************************************************************************** -# Copyright (C) 2011 Nicolas M. Thiery -# -# Distributed under the terms of the GNU General Public License (GPL) -# https://www.gnu.org/licenses/ -# ***************************************************************************** - -import os -import re -import sys - -from collections import defaultdict - - -def runsnake(command): - """ - Graphical profiling with ``runsnake``. - - INPUT: - - - ``command`` -- the command to be run as a string - - EXAMPLES:: - - sage: from sage.misc.dev_tools import runsnake - sage: runsnake("list(SymmetricGroup(3))") # optional - runsnake - - ``command`` is first preparsed (see :func:`preparse`):: - - sage: runsnake('for x in range(1,4): print(x^2)') # optional - runsnake - 1 - 4 - 9 - - :func:`runsnake` requires the program ``runsnake``. Due to non - trivial dependencies (python-wxgtk, ...), installing it within the - Sage distribution is unpractical. Hence, we recommend installing - it with the system wide Python. On Ubuntu 10.10, this can be done - with:: - - > sudo apt-get install python-profiler python-wxgtk2.8 python-setuptools - > sudo easy_install RunSnakeRun - - See the ``runsnake`` website for instructions for other platforms. - - :func:`runsnake` further assumes that the system wide Python is - installed in ``/usr/bin/python``. - - .. SEEALSO:: - - - `The runsnake website `_ - - ``%prun`` - - :class:`Profiler` - """ - import cProfile - from sage.misc.temporary_file import tmp_filename - from sage.misc.misc import get_main_globals - from sage.repl.preparse import preparse - tmpfile = tmp_filename() - cProfile.runctx(preparse(command.lstrip().rstrip()), get_main_globals(), - locals(), filename=tmpfile) - os.system("/usr/bin/python -E `which runsnake` %s &" % tmpfile) - - -def import_statement_string(module, names, lazy): - r""" - Return a (lazy) import statement for ``names`` from ``module``. - - INPUT: - - - ``module`` -- the name of a module - - - ``names`` -- list of 2-tuples containing names and alias to - import - - - ``lazy`` -- boolean; whether to return a lazy import statement - - EXAMPLES:: - - sage: import sage.misc.dev_tools as dt - sage: modname = 'sage.misc.dev_tools' - sage: names_and_aliases = [('import_statement_string', 'iss')] - sage: dt.import_statement_string(modname, names_and_aliases, False) - 'from sage.misc.dev_tools import import_statement_string as iss' - sage: dt.import_statement_string(modname, names_and_aliases, True) - "lazy_import('sage.misc.dev_tools', 'import_statement_string', 'iss')" - sage: dt.import_statement_string(modname, [('a','b'),('c','c'),('d','e')], False) - 'from sage.misc.dev_tools import a as b, c, d as e' - sage: dt.import_statement_string(modname, [(None,None)], False) - 'import sage.misc.dev_tools' - """ - if lazy: - if len(names) == 1: - name, alias = names[0] - if name == alias: - if name is None: - raise ValueError("cannot lazy import modules") - return "lazy_import('%s', '%s')" % (module, name) - else: - return "lazy_import('%s', '%s', '%s')" % (module, name, alias) - obj_names = "[" + ", ".join("'" + name[0] + "'" for name in names) + "]" - obj_aliases = "[" + ", ".join("'" + name[1] + "'" for name in names) + "]" - return "lazy_import('%s', %s, %s)" % (module, obj_names, obj_aliases) - else: - import_module = False - name_list = [] - for name, alias in names: - if name == alias: - if name is None: - import_module = True - continue - name_list.append(name) - else: - name_list.append("%s as %s" % (name, alias)) - res = [] - if import_module: - res.append("import %s" % module) - if name_list: - res.append("from %s import %s" % (module, ', '.join(name_list))) - return "\n".join(res) - - -def load_submodules(module=None, exclude_pattern=None): - r""" - Load all submodules of a given modules. - - This method is intended to be used by developers and especially the one - who uses :func:`import_statements`. By default it load the sage library and - it takes around a minute. - - INPUT: - - - ``module`` -- an optional module - - - ``exclude_pattern`` -- an optional regular expression pattern of module - names that have to be excluded - - EXAMPLES:: - - sage: sage.misc.dev_tools.load_submodules(sage.combinat) - load sage.combinat.algebraic_combinatorics... succeeded - ... - load sage.combinat.words.suffix_trees... succeeded - - Calling a second time has no effect (since the function does not import - modules already imported):: - - sage: sage.misc.dev_tools.load_submodules(sage.combinat) - - The second argument allows to exclude a pattern:: - - sage: sage.misc.dev_tools.load_submodules(sage.geometry, "database$|lattice") - load sage.geometry.cone... succeeded - load sage.geometry.cone_catalog... succeeded - load sage.geometry.fan_isomorphism... succeeded - ... - load sage.geometry.riemannian_manifolds.surface3d_generators... succeeded - - sage: sage.misc.dev_tools.load_submodules(sage.geometry) - load sage.geometry.polyhedron.lattice_euclidean_group_element... succeeded - load sage.geometry.polyhedron.palp_database... succeeded - load sage.geometry.polyhedron.ppl_lattice_polygon... succeeded - """ - from .package_dir import walk_packages - import importlib.util - - if module is None: - import sage - module = sage - exclude_pattern = r"^sage\.libs|^sage\.tests|tests$|^sage\.all_|all$|sage\.interacts$|^sage\.misc\.benchmark$" - - if exclude_pattern: - exclude = re.compile(exclude_pattern) - else: - exclude = None - - for importer, module_name, ispkg in walk_packages(module.__path__, module.__name__ + '.'): - if ispkg or module_name in sys.modules: - continue - - # we exclude several sage components because loading them is much of a - # problem... - if exclude and exclude.search(module_name): - continue - - try: - sys.stdout.write("load %s..." % module_name) - sys.stdout.flush() - # see - # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly - spec = importer.find_spec(module_name) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - sys.stdout.write(" succeeded\n") - except (ValueError, AttributeError, TypeError, ImportError): - # we might get error because of cython code that has been - # compiled but with source removed - sys.stdout.write("failed\n") - - -def find_objects_from_name(name, module_name=None, include_lazy_imports=False): - r""" - Return the list of objects from ``module_name`` whose name is ``name``. - - INPUT: - - - ``name`` -- string - - - ``module_name`` -- string or ``None``: - - - If ``module_name`` is ``None``, the function runs through all - loaded modules and returns the list of objects whose name matches ``name``. - - - If ``module_name`` is a string, then search only in submodules of - ``module_name``. - - - ``include_lazy_imports`` -- boolean (default: ``False``); whether to - include unresolved lazy imports (i.e., :class:`~sage.misc.lazy_import.LazyImport` - objects) in the output. - - In order to search through more modules you might use the function - :func:`load_submodules`. - - EXAMPLES:: - - sage: import sage.misc.dev_tools as dt - sage: dt.find_objects_from_name('FareySymbol') # needs sage.modular - [] - - sage: # needs sympy - sage: import sympy - sage: dt.find_objects_from_name('RR') - [Real Field with 53 bits of precision, RR] - sage: dt.find_objects_from_name('RR', 'sage') - [Real Field with 53 bits of precision] - sage: dt.find_objects_from_name('RR', 'sympy') - [RR] - - Examples that do not belong to the global namespace but in a loaded module:: - - sage: 'find_objects_from_name' in globals() - False - sage: objs = dt.find_objects_from_name('find_objects_from_name') - sage: len(objs) - 1 - sage: dt.find_objects_from_name is dt.find_objects_from_name - True - - When ``include_lazy_imports=True`` is used, several - :class:`~sage.misc.lazy_import.LazyImport` objects that are resolving to the - same object may be included in the output:: - - sage: dt.find_objects_from_name('RR', include_lazy_imports=True) # needs sage.rings.real_mpfr - [Real Field with 53 bits of precision, - ... - Real Field with 53 bits of precision, - RR] - - .. NOTE:: - - It might be a good idea to move this function into - :mod:`sage.misc.sageinspect`. - """ - from sage.misc.lazy_import import LazyImport - - obj = [] - for smodule_name, smodule in sys.modules.items(): - if module_name and not smodule_name.startswith(module_name): - continue - if hasattr(smodule, '__dict__') and name in smodule.__dict__: - u = smodule.__dict__[name] - if (not isinstance(u, LazyImport) or include_lazy_imports) and all(v is not u for v in obj): - obj.append(u) - - return obj - - -def find_object_modules(obj): - r""" - Return a dictionary whose keys are the names of the modules where ``obj`` - appear and the value at a given module name is the list of names that - ``obj`` have in that module. - - It is very unlikely that the output dictionary has several keys except when - ``obj`` is an instance of a class. - - EXAMPLES:: - - sage: from sage.misc.dev_tools import find_object_modules - sage: find_object_modules(RR) # needs sage.rings.real_mpfr - {'sage.rings.real_mpfr': ['RR']} - sage: find_object_modules(ZZ) - {'sage.rings.integer_ring': ['Z', 'ZZ']} - - .. NOTE:: - - It might be a good idea to move this function in - :mod:`sage.misc.sageinspect`. - """ - from sage.misc import sageinspect - - # see if the object is defined in its own module - # might be wrong for class instances as the instantiation might appear - # outside of the module !! - module_name = None - if sageinspect.isclassinstance(obj): - module_name = obj.__class__.__module__ - elif hasattr(obj, '__module__') and obj.__module__: - module_name = obj.__module__ - - if module_name: - if module_name not in sys.modules: - raise ValueError("this should never happen") - d = sys.modules[module_name].__dict__ - matching = sorted(key for key in d if d[key] is obj) - if matching: - return {module_name: matching} - - # otherwise, we parse all (already loaded) modules and hope to find - # something - module_to_obj = {} - for module_name, module in sys.modules.items(): - if module_name != '__main__' and hasattr(module, '__dict__'): - d = module.__dict__ - names = [key for key in d if d[key] is obj] - if names: - module_to_obj[module_name] = names - - # if the object is an instance, we try to guess where it is defined - if sageinspect.isclassinstance(obj): - dec_pattern = re.compile(r"^(\w[\w0-9\_]*)\s*=", re.MULTILINE) - module_to_obj2 = {} - for module_name, obj_names in module_to_obj.items(): - module_to_obj2[module_name] = [] - try: - src = sageinspect.sage_getsource(sys.modules[module_name]) - except TypeError: - pass - else: - m = dec_pattern.search(src) - while m: - if m.group(1) in obj_names: - module_to_obj2[module_name].append(m.group(1)) - m = dec_pattern.search(src, m.end()) - if not module_to_obj2[module_name]: - del module_to_obj2[module_name] - - if module_to_obj2: - return module_to_obj2 - - return module_to_obj - - -def import_statements(*objects, **kwds): - r""" - Print import statements for the given objects. - - INPUT: - - - ``*objects`` -- a sequence of objects or comma-separated strings of names - - - ``lazy`` -- boolean (default: ``False``); whether to print a lazy import - statement - - - ``verbose`` -- boolean (default: ``True``); whether to print information - in case of ambiguity - - - ``answer_as_str`` -- boolean (default: ``False``); if ``True`` return a - string instead of printing the statement - - EXAMPLES:: - - sage: import_statements(WeylGroup, lazy_attribute) # needs sage.libs.gap - from sage.combinat.root_system.weyl_group import WeylGroup - from sage.misc.lazy_attribute import lazy_attribute - - sage: import_statements(IntegerRing) - from sage.rings.integer_ring import IntegerRing - - If ``lazy`` is True, then :func:`lazy_import` statements are - displayed instead:: - - sage: import_statements(WeylGroup, lazy_attribute, lazy=True) # needs sage.libs.gap - from sage.misc.lazy_import import lazy_import - lazy_import('sage.combinat.root_system.weyl_group', 'WeylGroup') - lazy_import('sage.misc.lazy_attribute', 'lazy_attribute') - - In principle, the function should also work on object which are instances. - In case of ambiguity, one or two warning lines are printed:: - - sage: import_statements(RDF) - from sage.rings.real_double import RDF - - sage: import_statements(ZZ) - # ** Warning **: several names for that object: Z, ZZ - from sage.rings.integer_ring import Z - - sage: import_statements(euler_phi) - from sage.arith.misc import euler_phi - - sage: import_statements(x) # needs sage.symbolic - from sage.calculus.predefined import x - - If you don't like the warning you can disable them with the option ``verbose``:: - - sage: import_statements(ZZ, verbose=False) - from sage.rings.integer_ring import Z - - sage: import_statements(x, verbose=False) # needs sage.symbolic - from sage.calculus.predefined import x - - If the object has several names, an other way to get the import - statement you expect is to use a string instead of the object:: - - sage: import_statements(matrix) # needs sage.modules - # ** Warning **: several names for that object: Matrix, matrix - from sage.matrix.constructor import Matrix - - sage: import_statements('cached_function') - from sage.misc.cachefunc import cached_function - sage: import_statements('Z') - # **Warning**: distinct objects with name 'Z' in: - # - sage.calculus.predefined - # - sage.rings.integer_ring - from sage.rings.integer_ring import Z - - The strings are allowed to be comma-separated names, and parenthesis - are stripped for convenience:: - - sage: import_statements('(floor, ceil)') # needs sage.symbolic - from sage.functions.other import floor, ceil - - Specifying a string is also useful for objects that are not - imported in the Sage interpreter namespace by default. In this - case, an object with that name is looked up in all the modules - that have been imported in this session:: - - sage: import_statement_string - Traceback (most recent call last): - ... - NameError: name 'import_statement_string' is not defined - - sage: import_statements("import_statement_string") - from sage.misc.dev_tools import import_statement_string - - Sometimes objects are imported as an alias (from XXX import YYY as ZZZ) or - are affected (XXX = YYY) and the function might detect it:: - - sage: import_statements('FareySymbol') - from sage.modular.arithgroup.farey_symbol import Farey as FareySymbol - - sage: import_statements('power') - from sage.arith.power import generic_power as power - - In order to be able to detect functions that belong to a non-loaded module, - you might call the helper :func:`load_submodules` as in the following:: - - sage: import_statements('HeckeMonoid') - Traceback (most recent call last): - ... - LookupError: no object named 'HeckeMonoid' - sage: from sage.misc.dev_tools import load_submodules - sage: load_submodules(sage.monoids) - load sage.monoids.automatic_semigroup... succeeded - load sage.monoids.hecke_monoid... succeeded - load sage.monoids.indexed_free_monoid... succeeded - sage: import_statements('HeckeMonoid') - from sage.monoids.hecke_monoid import HeckeMonoid - - We test different objects which have no appropriate answer:: - - sage: import_statements('my_tailor_is_rich') - Traceback (most recent call last): - ... - LookupError: no object named 'my_tailor_is_rich' - sage: import_statements(5) - Traceback (most recent call last): - ... - ValueError: no import statement found for '5'. - - We test that it behaves well with lazy imported objects (:issue:`14767`):: - - sage: import_statements(NN) - from sage.rings.semirings.non_negative_integer_semiring import NN - sage: import_statements('NN') - from sage.rings.semirings.non_negative_integer_semiring import NN - - Deprecated lazy imports are ignored (see :issue:`17458`):: - - sage: lazy_import('sage.all', 'RR', 'deprecated_RR', namespace=sage.__dict__, deprecation=17458) - sage: import_statements('deprecated_RR') - Traceback (most recent call last): - ... - LookupError: object named 'deprecated_RR' is deprecated (see Issue #17458) - sage: lazy_import('sage.all', 'RR', namespace=sage.__dict__, deprecation=17458) - sage: import_statements('RR') - from sage.rings.real_mpfr import RR - - The following were fixed with :issue:`15351`:: - - sage: import_statements('Rationals') - from sage.rings.rational_field import RationalField as Rationals - sage: import_statements(sage.combinat.partition_algebra.SetPartitionsAk) - from sage.combinat.partition_algebra import SetPartitionsAk - sage: import_statements(CIF) - from sage.rings.cif import CIF - sage: import_statements(NaN) # needs sage.symbolic - from sage.symbolic.constants import NaN - sage: import_statements(pi) # needs sage.symbolic - from sage.symbolic.constants import pi - sage: import_statements('SAGE_ENV') - from sage.env import SAGE_ENV - sage: import_statements('graph_decompositions') - import sage.graphs.graph_decompositions - - Check that a name from the global namespace is properly found (see - :issue:`23779`):: - - sage: import_statements('log') - from sage.misc.functional import log - - .. NOTE:: - - The programmers try to made this function as smart as possible. - Nevertheless it is far from being perfect (for example it does not - detect deprecated stuff). So, if you use it, double check the answer and - report weird behaviors. - """ - import itertools - import inspect - from sage.misc.lazy_import import LazyImport - - answer = defaultdict(list) - module_name = None - # a dictionary module -> [(name1,alias1), (name2,alias2) ...] - # where "nameX" is an object in "module" that has to be - # imported with the alias "aliasX" - - lazy = kwds.pop("lazy", False) - verbose = kwds.pop("verbose", True) - answer_as_str = kwds.pop("answer_as_str", False) - - if kwds: - raise TypeError("Unexpected '{}' argument".format(next(iter(kwds)))) - - def expand_comma_separated_names(obj): - if isinstance(obj, str): - for w in obj.strip('()').split(','): - yield w.strip() - else: - yield obj - - for obj in itertools.chain.from_iterable(expand_comma_separated_names(object) - for object in objects): - name = None # the name of the object - - # 1. if obj is a string, we look for an object that has that name - if isinstance(obj, str): - from sage.all import sage_globals - G = sage_globals() - name = obj - if name in G: - # 1.a. object in the sage namespace - obj = [G[name]] - else: - # 1.b. object inside a submodule of sage - obj = find_objects_from_name(name, 'sage', include_lazy_imports=True) - if not obj: - # 1.c. object from something already imported - obj = find_objects_from_name(name, include_lazy_imports=True) - - # remove lazy imported objects from list obj - i = 0 - deprecation = None - while i < len(obj): - if isinstance(obj[i], LazyImport): - tmp = obj.pop(i) - # Ignore deprecated lazy imports - tmp_deprecation = tmp._get_deprecation_issue() - if tmp_deprecation: - deprecation = tmp_deprecation - else: - tmp = tmp._get_object() - if all(u is not tmp for u in obj): - obj.append(tmp) - else: - i += 1 - - if verbose and len(obj) > 1: - modules = set() - for o in obj: - modules.update(find_object_modules(o)) - print("# **Warning**: distinct objects with name '{}' " - "in:".format(name)) - for mod in sorted(modules): - print("# - {}".format(mod)) - - # choose a random object among the potentially enormous list of - # objects we get from "name" - try: - obj = obj[0] - except IndexError: - if deprecation: - raise LookupError( - "object named {!r} is deprecated (see Issue #" - "{})".format(name, deprecation)) - else: - raise LookupError("no object named {!r}".format(name)) - - # 1'. if obj is a LazyImport we recover the real object - if isinstance(obj, LazyImport): - obj = obj._get_object() - - # 2. Find out in which modules obj lives - # and update answer with a couple of strings "(name,alias)" where "name" is - # the name of the object in the module and "alias" is the name of the - # object - - # easy case: the object is itself a module - if inspect.ismodule(obj): - module_name = obj.__name__ - answer[module_name].append((None, None)) - continue - - modules = find_object_modules(obj) - if '__main__' in modules: - del modules['__main__'] - if '__mp_main__' in modules: - del modules['__mp_main__'] - - if not modules: - raise ValueError("no import statement found for '{}'.".format(obj)) - - if name is None: - # if the object is available under both ascii and unicode names, - # prefer the ascii version. - def is_ascii(s): - """ - Equivalent of `str.isascii` in Python >= 3.7 - """ - return all(ord(c) < 128 for c in s) - if any(is_ascii(s) - for (module_name, obj_names) in modules.items() - for s in obj_names): - for module_name, obj_names in list(modules.items()): - if any(not is_ascii(s) for s in obj_names): - obj_names = [name for name in obj_names if is_ascii(name)] - if not obj_names: - del modules[module_name] - else: - modules[module_name] = obj_names - - if len(modules) == 1: # the module is well defined - (module_name, obj_names), = modules.items() - if name is None: - if verbose and len(obj_names) > 1: - print("# ** Warning **: several names for that object: " - "{}".format(', '.join(sorted(obj_names)))) - name = alias = obj_names[0] - elif name in modules[module_name]: - alias = name - else: - alias = name - name = obj_names[0] - - answer[module_name].append((name, alias)) - continue - - # here modules contain several answers and we first try to see if there - # is a best one (i.e. the object "obj" is contained in the module and - # has name "name") - if name is not None: - good_modules = [mod for mod in modules if name in modules[mod]] - - if len(good_modules) == 1: - answer[good_modules[0]].append((name, name)) - continue - - # if the object is a class instance, it is likely that it is defined in - # some XYZ.all module - from .sageinspect import isclassinstance - if isclassinstance(obj): - module_name = type(obj).__module__ - i = module_name.rfind('.') - all_module_name = module_name[:i] + '.all' - if all_module_name in modules: - module_name = all_module_name - modules[module_name][0] - else: - module_name = None - - if module_name is None: - # here, either "obj" is a class instance but there is no natural - # candidate for its module or "obj" is not a class instance. - all_re = re.compile(r'.+\.all(?:_\w+)?$') - not_all_modules = [mod for mod in modules - if not all_re.match(mod)] - if not not_all_modules: - print("# ** Warning **: the object {} is only defined in " - ".all modules".format(obj)) - module_name = next(iter(modules)) - else: - if len(not_all_modules) > 1: - print("# ** Warning **: several modules for the object " - "{}: {}".format(obj, ', '.join(sorted(modules)))) - module_name = not_all_modules[0] - - # 3. Now that we found the module, we fix the problem of the alias - if name is None: - alias = name = modules[module_name][0] - else: - alias = name - name = modules[module_name][0] - - answer[module_name].append((name, alias)) - - res = [] - - if lazy: - res.append("from sage.misc.lazy_import import lazy_import") - - res.extend(import_statement_string(module_name, answer[module_name], lazy) - for module_name in sorted(answer)) - - if answer_as_str: - return '\n'.join(res) - else: - print('\n'.join(res)) diff --git a/src/sage/misc/meson.build b/src/sage/misc/meson.build index 97d4bf9e6a1..fbfc0644426 100644 --- a/src/sage/misc/meson.build +++ b/src/sage/misc/meson.build @@ -21,7 +21,6 @@ py.install_sources( 'cython.py', 'decorators.py', 'defaults.py', - 'dev_tools.py', 'edit_module.py', 'element_with_label.py', 'explain_pickle.py', @@ -62,7 +61,6 @@ py.install_sources( 'random_testing.py', 'randstate.pxd', 'remote_file.py', - 'replace_dot_all.py', 'repr.py', 'rest_index_of_methods.py', 'sage_eval.py', diff --git a/src/sage/misc/replace_dot_all.py b/src/sage/misc/replace_dot_all.py deleted file mode 100644 index ea51a9b3159..00000000000 --- a/src/sage/misc/replace_dot_all.py +++ /dev/null @@ -1,470 +0,0 @@ -r""" -Implementation of the command ``sage --fiximports``. - -This file provides a tool to fix the modularization antipattern ``namespace_pkg_all_import`` -reported by ``tox -e relint``. Sage library code should not import from ``sage.PAC.KAGE.all`` -when ``sage.PAC.KAGE`` is an implicit namespace package. - -AUTHORS: - -- Alex Chandler -- Matthias Köppe - -INSTRUCTIONS: - -To fix the above issue for all Python and Cython source files (``.py``, ``.pyx``, ``.pxi``) files in the ``src/sage`` directory, -run the following from a terminal in ``SAGE_ROOT`` :: - - ./sage -python src/sage/misc/replace_dot_all.py - -or :: - - ./sage --fiximports - -Running replace_dot_all.py will call the function :func:`walkdir_replace_dot_all` which walks through all Python and Cython source files -and replaces certain ``from sage.PAC.KAGE.all import something`` (those matching the pattern above) -with the correct ``import`` statement by applying the function :func:`~sage.misc.dev_tools.import_statements`. - -The user can also pass subdirectories of ``src/sage`` or specific files to fix. For example :: - - ./sage -python src/sage/misc/replace_dot_all.py src/sage/arith - -will fix all files in ``src/sage/arith`` and :: - - ./sage -python src/sage/misc/replace_dot_all.py src/sage/arith/functions.pyx - -will fix the file ``src/sage/arith/functions.py``. The file extension is necessary in the case of a specific file. The user can also -pass the verbose flag ``-v`` to print out the files being fixed. For example :: - - ./sage -python src/sage/misc/replace_dot_all.py -v src/sage/arith - -will fix all files in ``src/sage/arith`` and print out the unusual examples of ``import`` statements it finds. - -In some rare cases, such as ``import`` statements appearing in doctests, the program will not be able to fix the ``import`` statement. The program will -print out the location of the file and the line number of the exceptional ``import`` statement. The user can then manually fix the ``import`` statement. -The program will also (usually) print out the suggested replacement for the ``import`` statement. The user can then copy and paste this replacement -into the file. In the cases a suggested replacement is not printed out, the user should use the function :func:`~sage.misc.dev_tools.import_statements` -to find the correct ``import`` statement. -""" - -# **************************************************************************** -# Copyright (C) 2022-2023 Alex Chandler -# 2023 Matthias Koeppe -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# https://www.gnu.org/licenses/ -# **************************************************************************** - -# Importing packages - -from sage.misc.dev_tools import import_statements -import os -import re -import argparse - -# We import this using __import__ so that "tox -e relint" does not complain about this source file. -__import__("sage.all", globals(), locals(), ["*"]) - - -# Keep in sync with SAGE_ROOT/src/.relint.yml (namespace_pkg_all_import) - -default_package_regex = (r"sage(" - r"|[.](arith|categories|combinat|crypto|databases|data_structures|dynamics|ext|game_theory|games|geometry|graphs|groups|interfaces|manifolds|matrix|matroids|misc|modules|monoids|numerical|probability|quadratic_forms|quivers|rings|sat|schemes|sets|stats|tensor)[a-z0-9_.]*|[.]libs" - r")[.]all") - - -# Global variables - -examples = list('ABCDEFGHIJ') # controls how we print out interesting examples to the console -interesting_examples = dict(zip(examples, [0]*len(examples))) -log_messages = '' -number_examples_to_print = 100 # controls how many examples we print out to the console (100 effectively allows all unusual examples to be printed) -numberFiles, numberFilesMatchingRegex, numberFilesChanged, numberStatementsReplaced = 0, 0, 0, 0 # to print report on number of files changed - - -# Functions - -def find_replacements(location, package_regex=None, verbose=False): - r""" - Locate the lines in the file at ``location`` which contain an ``import`` statement. - - INPUT: - - - ``location`` -- a file path - - ``package_regex`` -- (default: :obj:`default_package_regex`) a regular expression matching - the ``sage.PAC.KAGE.all`` package names from which we do not want to import. - - ``verbose`` -- a parameter which if used will issue print statements when interesting examples are found - - OUTPUT: - - an array [row_index,import_index,replaced_commands,lines_spanned (optional)] with entries - - - ``row_index`` -- the row index (zero indexed) in the file which needs replacing - - ``import_index`` -- the index of row row_index which marks the beginning of the word ``import`` in the line - - ``replaced_commands`` -- the string which will replace the current line - - ``lines_spanned`` -- the number of lines the original statement spans - - This output can be processed by the function :func:`process_line`. - - EXAMPLES:: - - sage: # needs SAGE_SRC - sage: from sage.misc.replace_dot_all import * - sage: location = os.path.join(sage.env.SAGE_SRC, 'sage', 'plot', 'arc.py') - sage: find_replacements(location, package_regex='sage[.]plot[.]all', verbose=True) - [[..., ..., 'from sage.plot.graphics import Graphics']] - """ - if package_regex is None: - package_regex = default_package_regex - regex = r"from\s+" + package_regex + r"\s+import" - pattern = re.compile(regex) - replacements = [] - global log_messages, interesting_examples - with open(location) as fp: - skip_line = False - lines = fp.readlines() # read all lines using readline() - row_index = 0 - for row in lines: # iterate over all lines of python file - if pattern.search(row): # (match the regex also do not want to mess with documentation) - prefix = '' - if '*' in row or 'SAGE_ROOT' in row: - if verbose and interesting_examples['J'] < number_examples_to_print: - interesting_examples['J'] += 1 - log_messages += (f'J. Match but no changes made (import statement uses *) at {location}:{row_index + 1}. ' - f'Not applying any changes here.\n') - continue - elif not (row.lstrip()[0:4] == 'from'): - skip_line = True - if '"' not in row and "'" not in row: - print(f'\n' - f'NEED TO CHANGE MANUALLY \n' - f' Issue: line with import statement does not start with "from" \n' - f' Location: at {location} \n' - f' Line number: {row_index + 1}. \n' - f' Giving correct import statements:\n') - leading_space = 0 - while len(row) > 0 and row[leading_space] == ' ' and leading_space < len(row)-1: - leading_space += 1 - prefix_space = leading_space - while row[prefix_space:prefix_space+4] != 'from': - prefix_space += 1 - prefix = row[leading_space:prefix_space] - row = row[prefix_space:] - else: - print(f'\n' - f'NEED TO CHANGE MANUALLY \n' - f' Issue: import statement does not start with "from" and contains quotation marks \n' - f' Location: at {location}:{row_index + 1}. \n' - f' Not able to suggest correct import statements. User must use the function import_statements().') - continue - # find() method returns -1 if the value is not found, or if found it returns index of the first occurrence of the substring - import_index = row.find('import ') - modules = '' - to_exec = row.strip() # the import statement itself which we will clean and call to import modules e.g. "import (aa as a, bb, cc as c)" - to_eval = row[import_index + 7:-1].strip() # tuple of modules in import statement but e.g. "(aa as a, bb, cc as c)" is saved as "(a,bb,c)" - to_eval_raw = row[import_index + 7:-1].strip() # same as to_eval but we don't get rid of " as " parts e.g. "(aa as a, bb, cc as c)" - span = 0 # keeps track of how many lines the import statement spans - - if '(' in row: # for examples where we import a tuple of modules and the statement spans several lines - while ')' not in lines[row_index + span]: # finding the line which closes the import statement - span += 1 - to_exec += lines[row_index + span].strip() - to_eval += lines[row_index + span].strip() - to_eval_raw += lines[row_index + span].strip() - if span and verbose: # useful to see these multiline examples for debugging - if " as " in to_eval_raw and interesting_examples['D'] < number_examples_to_print: - log_messages += f'D. Interesting example (spans multiple lines and has " as ") at {location}:{row_index + 1}\n' - interesting_examples['D'] += 1 - elif interesting_examples['B'] < number_examples_to_print: - log_messages += f'B. Interesting example (spans multiple lines) at {location}:{row_index + 1}\n' - interesting_examples['B'] += 1 - - # if there is an "as" statement inside to_eval, we want to keep only the new name for the module e.g. "(aa as a, bb, cc as c)" becomes "(a,bb,c)" - while " as " in to_eval: - as_ind = to_eval.find(" as ") - j = as_ind - 1 - while to_eval[j] not in [',', ' '] and j >= 0: - j -= 1 - to_eval = to_eval[0:j+1] + to_eval[as_ind+4:] - - try: # trying to execute the import statement so we can eval the modules and feed them to the function import_statements - to_exec = to_exec.replace("'", '').replace('"', '') - if (to_exec[-1] == ','): - to_exec = to_exec[:-1] - exec(to_exec) - except ModuleNotFoundError as err: - print(f'ModuleNotFoundError: {err} found when trying to execute {to_exec}') - except ImportError as err: - print(f'ImportError: {err} found when trying to execute {to_exec}') - - try: # try to evaluate the list of module names to get a list of the modules themselves which we can call import_statements on - modules = eval(to_eval) - except NameError as err: - print(f'NameError: {err} found when trying to evaluate {to_eval} at {location}:{row_index + 1}') - except SyntaxError as err: - print(f'SyntaxError: {err} found when trying to evaluate {to_eval} at {location}:{row_index + 1}') - - # Need module to be a list of modules we are importing. If a single module was given, we make it a 1-element list. - if not isinstance(modules, tuple): - modules = [modules] - - to_eval_list = to_eval.replace('(', '').replace(')', '').split(',') # convert comma separated string to_eval to a list - to_eval_list_raw = to_eval_raw.replace('(', '').replace(')', '').split(',') # convert comma separated string to_eval_raw to a list - to_eval_list_index = 0 - - change_to = '' - # constructs the appropriate replacement for the import statement and stores it (along with location data) in list replacements - for mod in modules: - postfix = '' - as_index = -1 - # saves the callable name of module in variable postfix (e.g. for module "b" called by "bb as b" we set postfix = " as b") - if " as " in to_eval_list_raw[to_eval_list_index]: - if verbose and interesting_examples['C'] < number_examples_to_print: - log_messages += f'C. Interesting example (" as " in tuple import) at {location}:{row_index + 1}\n' - interesting_examples['C'] += 1 - as_index = to_eval_list_raw[to_eval_list_index].index(" as ") - postfix = to_eval_list_raw[to_eval_list_index][as_index:] - new_import_statement = import_statements(mod, answer_as_str=True, verbose=False) # import statement for the current mod in the list module - import_index = new_import_statement.find('import') - new_mod_as_string = new_import_statement[import_index + 7:].strip() # the name for the module given by the function import_statements - if as_index >= 0: - # the name for the module as originally called in the document (when there is an " as " statement) - original_mod_string = to_eval_list_raw[to_eval_list_index].strip()[:as_index] - else: - original_mod_string = to_eval_list[to_eval_list_index].strip() # the name for the module as originally called in the document - if original_mod_string != new_mod_as_string: # if the names differ, we use the original name as it was called in the document - if verbose and interesting_examples['A'] < number_examples_to_print: - log_messages += (f'A. Interesting example (module has multiple names) at {location}:{row_index + 1}. ' - f'Names: {original_mod_string}, {new_mod_as_string}. ' - f'Replacing new {new_mod_as_string} by original {original_mod_string}.\n') - interesting_examples['A'] += 1 - new_import_statement = new_import_statement.replace(' ' + new_mod_as_string, ' ' + new_mod_as_string + ' as ' + original_mod_string) - if " as " in postfix and interesting_examples['G'] < number_examples_to_print: - log_messages += (f'G. Interesting example (module has multiple names) at {location}:{row_index + 1}. ' - f'Names: {original_mod_string}, {new_mod_as_string}. ' - f'Replacing new {new_mod_as_string} by original {original_mod_string}.\n') - interesting_examples['G'] += 1 - if len(postfix.strip()) > 0: # if module was called with " as " statement, we put that back in by adding the string "postfix" - # if " as " in new_import_statement locate the index of " as ", remove the end after this, and add the postfix there - if " as " in new_import_statement: - new_import_statement = new_import_statement[:new_import_statement.index(" as ")] + ' ' + postfix.strip() - else: - new_import_statement += (' ' + postfix.strip()) - change_to += (prefix + new_import_statement + '\n') - to_eval_list_index += 1 - # [:-1] on change_to gets rid of the last '\n' we added which adds an unnecessary new line - replacement = [row_index, import_index, change_to[:-1]].copy() - if span: - # if original statement spanned multiple lines, we store that information to signal that we need to skip lines - # as we read the document in the function make_replacements_in_file - replacement.append(span) - if not skip_line: - replacements.append(replacement) - row_index += 1 - skip_line = False - # keeping track of the numbers of files changed and statements replaced - global numberStatementsReplaced, numberFilesChanged - numberStatementsReplaced += len(replacements) - if replacements: - numberFilesChanged += 1 - return replacements - - -def process_line(location, line, replacements, row_index, verbose=False): - r""" - Modify a single source code ``line`` according to the given ``replacements``. - - INPUTS: - - - ``location`` -- a file path; only used for logging - - ``line`` -- a source code line - - ``replacements`` -- the array output from :func:`find_replacements` - - ``row_index`` -- the line number where ``import`` appears - - ``verbose`` -- if ``True``, issue print statements when interesting - examples are found - - OUTPUT: an array ``[new_line, replacements]`` with entries - - - ``new_line`` -- the modified import statement (possibly now on several lines) - - ``replacements`` -- just returns the original replacements with its index 0 element removed if ``replacements`` is nonempty - - EXAMPLES: - - Replacing the first line which needs a replacement in the source file with filepath ``src/sage/plot/arc.py``:: - - sage: # needs SAGE_SRC - sage: from sage.misc.replace_dot_all import * - sage: location = os.path.join(sage.env.SAGE_SRC, 'sage', 'plot', 'arc.py') - sage: replacements = find_replacements(location, package_regex='sage[.]plot[.]all', verbose=True); replacements - [[477, 24, 'from sage.plot.graphics import Graphics']] - sage: with open(location, "r") as file: - ....: lines = file.readlines() - sage: row_index, col_number, *_ = replacements[0] - sage: line = lines[row_index] - sage: print(line.rstrip()) - from sage.plot.all import Graphics - sage: new_line, replacements = process_line(location, line, replacements, row_index) - sage: print(new_line) - from sage.plot.graphics import Graphics - sage: replacements - [] - """ - line = line.rstrip() # stripping line break - new_line = '' - global log_messages, interesting_examples - if len(replacements) == 0: - return line, replacements - if row_index == replacements[0][0]: # if line marked as containing .all - replacement = replacements.pop(0) - leading_space = 0 - while line and line[leading_space] == ' ' and leading_space < len(line)-1: - leading_space += 1 - new_line = ' '*leading_space + replacement[2] # adds leading space to first line (which may or may not start with 'from') - new_line = new_line.replace('\n', '\n'+' '*leading_space) # adds correct amount of indentation to the replacement at each line - # new_line = replacement[2].replace('from ',' '*leading_space + 'from ') # adds correct amount of indentation to the replacement at each line - if verbose and leading_space > 0: - if len(replacement) == 4 and interesting_examples['F'] < number_examples_to_print: - log_messages += f'F. Interesting example (has leading space and multiline) at {location}:{replacement[0] + 1}\n' - interesting_examples['F'] += 1 - elif interesting_examples['E'] < number_examples_to_print: - log_messages += f'E. Interesting example (has leading space) at {location}:{replacement[0] + 1}\n' - interesting_examples['E'] += 1 - - else: # if line does not contain .all - new_line = line - return new_line, replacements - - -def make_replacements_in_file(location, package_regex=None, verbose=False, output=None): - r""" - Replace ``import`` statements in the file with filepath "location". - - INPUT: - - - ``location`` -- a file path - - ``package_regex`` -- (default: :obj:`default_package_regex`) a regular expression matching - the ``sage.PAC.KAGE.all`` package names from which we do not want to import. - - ``verbose`` -- if ``True``, issue print statements when interesting examples are found - - ``output`` -- a file path; if ``None``, overwrite the file given by ``location`` - - EXAMPLES:: - - sage: from sage.misc.replace_dot_all import * - sage: import tempfile - sage: with tempfile.TemporaryDirectory() as d: - ....: location = os.path.join(d, "input.py") - ....: with open(location, "w") as input: - ....: _ = input.write("from sage.plot.all import point2d\n") - ....: _ = input.write("from sage.plot.line import line\n") - ....: make_replacements_in_file(location, 'sage[.]plot[.]all', True) - ....: with open(location, "r") as output: - ....: for line in output: - ....: print(line.strip()) - from sage.plot.point import point2d - from sage.plot.line import line - """ - replacements = find_replacements(location, package_regex, verbose) - with open(location) as file: - lines = file.readlines() - replaced_content = "" - row_index = 0 # keeps track of the line number - while row_index < len(lines): # looping through the file - line = lines[row_index] - span = 0 # keeps track of number of lines import statement spans - if replacements and row_index == replacements[0][0] and len(replacements[0]) == 4: - span = replacements[0][3] # if import statement spans span lines - # returns the line if no replacements are needed and returns the processed line otherwise - new_line, replacements = process_line(location, line, replacements, row_index, verbose=verbose) - replaced_content += new_line + "\n" # concatenate the new string and add an end-line break - row_index += 1 + span - if output is None: - output = location - with open(output, "w") as write_file: # Open file in write mode - write_file.write(replaced_content) # overwriting the old file contents with the new/replaced content - - -def walkdir_replace_dot_all(dir, file_regex=r'.*[.](py|pyx|pxi)$', package_regex=None, verbose=False, *, - excluded_file_regex=r'auto-methods|replace_dot_all'): - r""" - Replace ``import`` statements in the files in directory ``dir`` matching the regex pattern ``file_regex``. - - INPUTS: - - - ``dir`` -- a directory path - - ``file_regex`` -- a regular expression matching the file names to process - - ``package_regex`` -- (default: :obj:`default_package_regex`) a regular expression matching - the ``sage.PAC.KAGE.all`` package names from which we do not want to import. - - ``verbose`` -- if ``True``, print statements when interesting examples are found - - ``excluded_file_regex`` -- a regular expression matching the file names to exclude - - EXAMPLES:: - - sage: # needs SAGE_SRC - sage: from sage.misc.replace_dot_all import * - sage: walkdir_replace_dot_all(os.path.join(sage.env.SAGE_SRC, 'sage')) # not tested - """ - global numberFiles, numberFilesMatchingRegex - file_regex = re.compile(file_regex) - excluded_file_regex = re.compile(excluded_file_regex) - for root, dirs, files in os.walk(dir, topdown=False): - for name in files: - numberFiles += 1 - if file_regex.search(name) and not excluded_file_regex.search(name): - numberFilesMatchingRegex += 1 - location = os.path.join(root, name) - make_replacements_in_file(location, package_regex, verbose) - - -# ******************************************************** EXECUTES MAIN FUNCTION ********************************************************************** -# this executes the main function in this file which writes over all import statements matching regex in files in specified location matching fileRegex: -if __name__ == "__main__": - # Create argument parser - parser = argparse.ArgumentParser() - # Optional arguments - parser.add_argument( - "location", - metavar='files or directories', - nargs='*', - help=("Names of source directories or source files. " - "If none given, walks through all files in src/sage."), - type=str) - parser.add_argument( - "-v", "--verbose", - help="Increase output verbosity. Shows locations of any unusual cases of import statements and the corresponding changes.", - action="store_true") # Parse arguments - args = parser.parse_args() - verbosity = args.verbose - # Declare regular expressions - file_regex = r'.*[.](py|pyx|pxi)$' - package_regex = None - # Execute the main function based on the specified location and verbosity - if not args.location: - from sage.env import SAGE_SRC - - args.location = [os.path.join(SAGE_SRC, 'sage')] - try: - for location in args.location: - if not (location.endswith('.py') or location.endswith('.pxi')): - # Assume directory - walkdir_replace_dot_all(location, file_regex, package_regex, verbose=verbosity) - else: - # make replacements in file specified by location argument - make_replacements_in_file(location, package_regex, verbose=verbosity) - finally: - # Print report also when interrupted - if verbosity: - log_messages_split = sorted(log_messages.rstrip().split('\n')) - for i, message in enumerate(log_messages_split, start=1): - # add index to each line - print(f'{i}. {message.rstrip()}') - report = 'REPORT:\n' - report += f'Number of files checked: {numberFiles}\n' - report += f'Number of files matching regex: {numberFilesMatchingRegex}\n' - report += f'Number of files changed: {numberFilesChanged}\n' - report += f'Number of import statements replaced: {numberStatementsReplaced}' - print('*'*100 + '\n' + report + '\n' + '*'*100) - -# ****************************************************************************************************************************************************** From ecc280afeede29dd31a4c90d811df036f0af7ff0 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Sun, 5 Jan 2025 02:50:39 +0800 Subject: [PATCH 2/3] fix import --- src/sage/misc/all.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sage/misc/all.py b/src/sage/misc/all.py index fa14ec98696..258ccf762e0 100644 --- a/src/sage/misc/all.py +++ b/src/sage/misc/all.py @@ -15,8 +15,6 @@ from sage.misc.banner import version -from sage.misc.dev_tools import import_statements - from sage.misc.html import html, pretty_print_default from sage.misc.table import table @@ -126,7 +124,6 @@ lazy_import('sage.misc.inline_fortran', 'fortran') lazy_import('sage.misc.banner', 'banner', deprecation=34259) -lazy_import('sage.misc.dev_tools', 'runsnake', deprecation=34259) lazy_import('sage.misc.edit_module', 'set_edit_template', deprecation=34259) lazy_import('sage.misc.profiler', 'Profiler', deprecation=34259) lazy_import('sage.misc.trace', 'trace', deprecation=34259) From 91d79d9d6e25a699afa0e8dbe05aae64ad0172b4 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Wed, 12 Feb 2025 13:57:56 +0100 Subject: [PATCH 3/3] Fix tests --- src/doc/en/faq/faq-contribute.rst | 7 +-- src/doc/it/faq/faq-contribute.rst | 8 +--- src/sage/features/all.py | 2 +- src/sage/misc/sageinspect.py | 74 +++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/doc/en/faq/faq-contribute.rst b/src/doc/en/faq/faq-contribute.rst index 644715a0435..b06d1d3efd2 100644 --- a/src/doc/en/faq/faq-contribute.rst +++ b/src/doc/en/faq/faq-contribute.rst @@ -247,12 +247,7 @@ necessity to import what you need. from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - You can use ``import_statements`` to get the exact necessary line:: - - sage: import_statements(PolynomialRing) - from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - - If this fails, you can ask Sage where to find ``PolynomialRing`` using:: + You can ask Sage where to find ``PolynomialRing`` using:: sage: PolynomialRing.__module__ 'sage.rings.polynomial.polynomial_ring_constructor' diff --git a/src/doc/it/faq/faq-contribute.rst b/src/doc/it/faq/faq-contribute.rst index 3b6c1612083..886d56c181f 100644 --- a/src/doc/it/faq/faq-contribute.rst +++ b/src/doc/it/faq/faq-contribute.rst @@ -233,13 +233,7 @@ preparser di Sage) e la necessità di importare quello che ti serve. from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - Puoi chiedere a Sage dove il comando per importare ``PolynomialRing`` usando:: - - sage: import_statements(PolynomialRing) - from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - - Se questo fallisce, puoi chiedere a Sage dove si trove ``PolynomialRing`` - usando:: + Puoi chiedere a Sage dove si trove ``PolynomialRing`` usando:: sage: PolynomialRing.__module__ 'sage.rings.polynomial.polynomial_ring_constructor' diff --git a/src/sage/features/all.py b/src/sage/features/all.py index 14d2480d520..8f200933e05 100644 --- a/src/sage/features/all.py +++ b/src/sage/features/all.py @@ -117,7 +117,7 @@ def name_feature(name, toplevel=None): except AttributeError: return None - from sage.misc.dev_tools import find_object_modules + from sage.misc.sageinspect import find_object_modules for module, names in find_object_modules(obj).items(): if name in names and (feature := module_feature(module)): diff --git a/src/sage/misc/sageinspect.py b/src/sage/misc/sageinspect.py index 4f7aed2820f..eabbcde4842 100644 --- a/src/sage/misc/sageinspect.py +++ b/src/sage/misc/sageinspect.py @@ -112,6 +112,7 @@ class definition be found starting from the ``__init__`` method. import inspect import functools import os +import sys import tokenize import re @@ -2577,3 +2578,76 @@ def test3(b, # 12 sage: _extract_embedded_position(s) is None True """ + +def find_object_modules(obj): + r""" + Return a dictionary whose keys are the names of the modules where ``obj`` + appear and the value at a given module name is the list of names that + ``obj`` have in that module. + + It is very unlikely that the output dictionary has several keys except when + ``obj`` is an instance of a class. + + EXAMPLES:: + + sage: from sage.misc.dev_tools import find_object_modules + sage: find_object_modules(RR) # needs sage.rings.real_mpfr + {'sage.rings.real_mpfr': ['RR']} + sage: find_object_modules(ZZ) + {'sage.rings.integer_ring': ['Z', 'ZZ']} + + .. NOTE:: + + It might be a good idea to move this function in + :mod:`sage.misc.sageinspect`. + """ + # see if the object is defined in its own module + # might be wrong for class instances as the instantiation might appear + # outside of the module !! + module_name = None + if isclassinstance(obj): + module_name = obj.__class__.__module__ + elif hasattr(obj, '__module__') and obj.__module__: + module_name = obj.__module__ + + if module_name: + if module_name not in sys.modules: + raise ValueError("this should never happen") + d = sys.modules[module_name].__dict__ + matching = sorted(key for key in d if d[key] is obj) + if matching: + return {module_name: matching} + + # otherwise, we parse all (already loaded) modules and hope to find + # something + module_to_obj = {} + for module_name, module in sys.modules.items(): + if module_name != '__main__' and hasattr(module, '__dict__'): + d = module.__dict__ + names = [key for key in d if d[key] is obj] + if names: + module_to_obj[module_name] = names + + # if the object is an instance, we try to guess where it is defined + if isclassinstance(obj): + dec_pattern = re.compile(r"^(\w[\w0-9\_]*)\s*=", re.MULTILINE) + module_to_obj2 = {} + for module_name, obj_names in module_to_obj.items(): + module_to_obj2[module_name] = [] + try: + src = sage_getsource(sys.modules[module_name]) + except TypeError: + pass + else: + m = dec_pattern.search(src) + while m: + if m.group(1) in obj_names: + module_to_obj2[module_name].append(m.group(1)) + m = dec_pattern.search(src, m.end()) + if not module_to_obj2[module_name]: + del module_to_obj2[module_name] + + if module_to_obj2: + return module_to_obj2 + + return module_to_obj