diff --git a/cirq-core/cirq/_compat.py b/cirq-core/cirq/_compat.py index facfdd8e45a..39e0891cc77 100644 --- a/cirq-core/cirq/_compat.py +++ b/cirq-core/cirq/_compat.py @@ -493,7 +493,6 @@ class DeprecatedModuleFinder(importlib.abc.MetaPathFinder): It is meant to be used as a wrapper around existing MetaPathFinder instances. Args: - finder: The finder to wrap. new_module_name: The new module's fully qualified name. old_module_name: The deprecated module's fully qualified name. deadline: The deprecation deadline. @@ -503,35 +502,18 @@ class DeprecatedModuleFinder(importlib.abc.MetaPathFinder): def __init__( self, - finder: Any, new_module_name: str, old_module_name: str, deadline: str, broken_module_exception: Optional[BaseException], ): - """An aliasing module finder that uses an existing module finder to find a python + """An aliasing module finder that uses existing module finders to find a python module spec and intercept the execution of matching modules. """ - self.finder = finder self.new_module_name = new_module_name self.old_module_name = old_module_name self.deadline = deadline self.broken_module_exception = broken_module_exception - # to cater for metadata path finders - # https://docs.python.org/3/library/importlib.metadata.html#extending-the-search-algorithm - if hasattr(finder, "find_distributions"): - - def find_distributions(context): - return self.finder.find_distributions(context) - - self.find_distributions = find_distributions - if hasattr(finder, "invalidate_caches"): - - def invalidate_caches() -> None: - return self.finder.invalidate_caches() - - # mypy#2427 - self.invalidate_caches = invalidate_caches # type: ignore def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any: """Finds the specification of a module. @@ -547,8 +529,7 @@ def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any: to the wrapped finder. """ if fullname != self.old_module_name and not fullname.startswith(self.old_module_name + "."): - # if we are not interested in it, then just pass through to the wrapped finder - return self.finder.find_spec(fullname, path, target) + return None if self.broken_module_exception is not None: raise self.broken_module_exception @@ -557,33 +538,8 @@ def find_spec(self, fullname: str, path: Any = None, target: Any = None) -> Any: new_fullname = self.new_module_name + fullname[len(self.old_module_name) :] - # find the corresponding spec in the new structure - if fullname == self.old_module_name: - # this is the first time the deprecated module is being found - # which means that the new parent needs to be found first and under - # the new parent's path, we should be able to find the new name of - # the deprecated module - # this code is heavily inspired by importlib.util.find_spec - parent_name = new_fullname.rpartition('.')[0] - if parent_name: - parent = __import__(parent_name, fromlist=['__path__']) - # note that compared to importlib.util.find_spec we don't handle - # AttributeError here because it is not expected to happen in case - # of a DeprecatedModuleLoader - the new parent should exist and be - # a proper package - parent_path = parent.__path__ - else: - parent_path = None - spec = self.finder.find_spec(new_fullname, parent_path, None) - else: - # we are finding a submodule of the parent of the deprecated module, - # which means that the parent was already found, and thus, `path` is - # correctly pointing to the module's parent in the new hierarchy - spec = self.finder.find_spec( - new_fullname, - path=path, - target=target, - ) + # use normal import mechanism for the new module specs + spec = importlib.util.find_spec(new_fullname) # if the spec exists, return the DeprecatedModuleLoader that will do the loading as well # as set the alias(es) in sys.modules as necessary @@ -666,21 +622,10 @@ def deprecated_submodule( _BrokenModule(new_module_name, broken_module_exception), ) - def wrap(finder: Any) -> Any: - # Sphinx looks for non-wrapped MockFinders - # so we have to check for them and not wrap them - if 'sphinx' in sys.modules: - from sphinx.ext.autodoc.mock import MockFinder - - if isinstance(finder, MockFinder): - return finder - if not hasattr(finder, 'find_spec'): - return finder - return DeprecatedModuleFinder( - finder, new_module_name, old_module_name, deadline, broken_module_exception - ) - - sys.meta_path = [wrap(finder) for finder in sys.meta_path] + finder = DeprecatedModuleFinder( + new_module_name, old_module_name, deadline, broken_module_exception + ) + sys.meta_path.append(finder) def _setup_deprecated_submodule_attribute( diff --git a/cirq-core/cirq/_compat_test.py b/cirq-core/cirq/_compat_test.py index b0c84bc7e9f..c2314c3e9f5 100644 --- a/cirq-core/cirq/_compat_test.py +++ b/cirq-core/cirq/_compat_test.py @@ -21,10 +21,9 @@ import types import warnings from types import ModuleType -from typing import Any, Callable, List, Optional, Sequence, Union +from typing import Any, Callable, Optional from importlib.machinery import ModuleSpec from unittest import mock -from importlib.abc import MetaPathFinder import numpy as np @@ -44,7 +43,6 @@ deprecated_class, deprecated_submodule, DeprecatedModuleLoader, - DeprecatedModuleFinder, DeprecatedModuleImportError, ) @@ -673,6 +671,25 @@ def _test_metadata_search_path_inner(): assert m.metadata('flynt') +def test_metadata_distributions_after_deprecated_submodule(): + subprocess_context(_test_metadata_distributions_after_deprecated_submodule)() + + +def _test_metadata_distributions_after_deprecated_submodule(): + # verify deprecated_submodule does not break importlib_metadata.distributions() + # See https://github.com/quantumlib/Cirq/issues/4729 + deprecated_submodule( + new_module_name='cirq.neutral_atoms', + old_parent='cirq', + old_child='swiss_atoms', + deadline="v0.14", + create_attribute=True, + ) + m = pytest.importorskip("importlib_metadata") + distlist = list(m.distributions()) + assert all(isinstance(d.name, str) for d in distlist) + + def test_type_repr_in_new_module(): # to cater for metadata path finders # https://docs.python.org/3/library/importlib.metadata.html#extending-the-search-algorithm @@ -850,18 +867,6 @@ def module_repr(self, module: ModuleType) -> str: ) -def test_invalidate_caches(): - called = False - - class FakeFinder(importlib.abc.MetaPathFinder): - def invalidate_caches(self) -> None: - nonlocal called - called = True - - DeprecatedModuleFinder(FakeFinder(), 'new', 'old', 'v0.1', None).invalidate_caches() - assert called - - def test_subprocess_test_failure(): with pytest.raises(Failed, match='ValueError.*this fails'): subprocess_context(_test_subprocess_test_failure_inner)() @@ -882,68 +887,3 @@ def _dir_is_still_valid_inner(): for m in ['fake_a', 'info', 'module_a', 'sys']: assert m in dir(mod) - - -class MockModule(ModuleType): - def __init__(self, module_name: str): - ModuleType.__init__(self, module_name) - if '.' in module_name: - package, module = module_name.rsplit('.', 1) - setattr(get_mock_module(package), module, self) - - def _initialize_(self, module_code: types.FunctionType): - self.__dict__.update(module_code(self.__name__)) - - -def get_mock_module(module_name: str) -> ModuleType: - if module_name not in sys.modules: - sys.modules[module_name] = MockModule(module_name) - return sys.modules[module_name] - - -def modulize(module_name: str) -> Callable[[types.FunctionType], Any]: - """Converts a function into a module: - https://stackoverflow.com/a/45421428/5716192 - """ - return get_mock_module( - module_name - )._initialize_ # type: ignore # mypy can't detect the _initialize_ method - # from the MockModule in sys.modules[module_name] - - -def test_deprecated_module_does_not_wrap_mockfinder(): - @modulize('sphinx.ext.autodoc.mock') - def module_code( # pylint: disable=unused-variable # https://github.com/PyCQA/pylint/issues/2842 - __name__, - ): # pylint: disable=redefined-builtin - - # put module code here - class MockFinder(MetaPathFinder): - def __init__(self, modnames: List[str]) -> None: - super().__init__() - - def find_spec( - self, - fullname: Optional[str] = None, - path: Optional[Sequence[Union[bytes, str]]] = None, - target: Optional[ModuleType] = None, - ) -> None: - pass - - # the function must return locals() - return locals() - - from sphinx.ext.autodoc.mock import MockFinder - - fake_mockfinder = MockFinder([]) - sys.meta_path.insert(0, fake_mockfinder) - deprecated_submodule( - new_module_name='sphinx_1', - old_parent='sphinx_2', - old_child='old_ch', - deadline='v1.2', - create_attribute=False, - ) - assert fake_mockfinder in sys.meta_path - # Cleanup sys.metapath after test - sys.meta_path.remove(fake_mockfinder) diff --git a/dev_tools/conf/mypy.ini b/dev_tools/conf/mypy.ini index 86bb147cb34..8d238b58848 100644 --- a/dev_tools/conf/mypy.ini +++ b/dev_tools/conf/mypy.ini @@ -28,7 +28,3 @@ follow_imports_for_stubs = true # ruamel is a downstream dependency of cirq-rigetti through pyquil. [mypy-ruamel.*] ignore_missing_imports = true - -# Mocked module in cirq/_compat.py -[mypy-sphinx.*] -ignore_missing_imports = true diff --git a/dev_tools/requirements/deps/pytest.txt b/dev_tools/requirements/deps/pytest.txt index 8f6b17f739f..9c48432afcd 100644 --- a/dev_tools/requirements/deps/pytest.txt +++ b/dev_tools/requirements/deps/pytest.txt @@ -11,12 +11,12 @@ filelock~=3.0.12 # For testing time specific logic freezegun~=0.3.15 -# for python 3.7 and below needs to be installed -importlib-metadata; python_version < '3.8' +# For testing interference between deprecated_submodule and importlib_metadata +importlib-metadata # codeowners test dependency codeowners; python_version >= '3.7' # for creating isolated environments virtualenv -virtualenv-clone \ No newline at end of file +virtualenv-clone