Skip to content

Instantly share code, notes, and snippets.

@waveform80
Created March 2, 2018 18:13
Show Gist options
  • Save waveform80/c9d7c9f81d6b67028c90eac5f4080053 to your computer and use it in GitHub Desktop.
Save waveform80/c9d7c9f81d6b67028c90eac5f4080053 to your computer and use it in GitHub Desktop.
A quick hack to see if it's feasible to detect shadowed modules on import

Shadow.py

This is a rough'n'ready experiment to see if it's feasible to detect shadowed modules on import. A shadowed module is one where a user has inadvertently named a module the same as a module on the standard search path (a common example in education is "turtle.py" and my own unfortunately named "picamera.py" :).

If this module is imported, it tweaks the standard import machinery to check for such shadowing the first time a module is imported. If it finds any, it raises ShadowWarning (which descends from ImportWarning) giving the path of the imported module, and the path of the first module it is shadowing. For example:

$ touch turtle.py
$ touch numpy.py
$ python3
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import shadow
>>> import turtle
/home/dave/shadow.py:35: ShadowWarning: /home/dave/turtle.py shadows /usr/lib/python3.5/turtle.py
  result.origin, conflict.origin)))
>>> import numpy
/home/dave/shadow.py:35: ShadowWarning: /home/dave/numpy.py shadows /usr/lib/python3/dist-packages/numpy/__init__.py
  result.origin, conflict.origin)))
>>> 
$ rm turtle.py
$ rm numpy.py

Caveats

I should add that this will only work under Python 3. In fact, I think it'll only work under Python 3.5 and above (some bits of the import machinery changed in 3.5 and those are the bits I've overridden, though it could probably be refined to work in earlier versions too).

Another important note is that this will slow down import times (obviously) as every first time import will require additional disk accesses to check for shadow conflicts. Therefore I wouldn't advocate this as a general solution but it might interesting to add to systems geared towards education, e.g. the Mu Editor?

import sys
import warnings
import importlib.machinery
from pathlib import Path
class ShadowWarning(ImportWarning):
"Warning raised when an import shadows another module later in sys.path"
class ShadowPathFinder(importlib.machinery.PathFinder):
@classmethod
def path_suffix(cls, origin, path):
origin_path = Path(origin)
for ix, search_path in enumerate(path):
search_path = Path(search_path).resolve()
try:
origin_path.relative_to(search_path)
except ValueError:
continue
else:
return path[ix + 1:]
raise ValueError('unable to find path of {}'.format(origin))
@classmethod
def find_spec(cls, fullname, path=None, target=None):
result = super().find_spec(fullname, path, target)
if result is not None and result.has_location:
if path is None:
path = sys.path
path = cls.path_suffix(result.origin, path)
conflict = super().find_spec(fullname, path, target)
if conflict is not None:
warnings.warn(ShadowWarning('{} shadows {}'.format(
result.origin, conflict.origin)))
return result
# Replace PathFinder in the list of meta path finders currently registered
# (it's usually the last one).
sys.meta_path = [
ShadowPathFinder if finder is importlib.machinery.PathFinder else finder
for finder in sys.meta_path
]
# ImportWarning is ignored by default but it's a reasonable assumption that
# anyone importing this module actually wants to see ShadowWarning at least...
warnings.simplefilter('default', category=ShadowWarning)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment