File: reloadall3.py

"""
===========================================================================
reloadall3.py: transitively reload nested modules (explicit stack).

A May 2020 update to an example from Learning Python, 5th Edition.
This version demos three recodings that fix a problem in the original.
Comments are errata-page replies to the reader who found the issue.
Run this file directly to see the output of its four alternatives.
===========================================================================
"""

import types
from imp import reload                              # from required in 3.X
from reloadall import status, tryreload, tester


"""
===========================================================================
You're right (and great catch! - I wish this had been reported earlier).  
As coded in the book originally, reloadall3 can indeed reload a module 
more than once, because it checks only modules already visited, not those 
currently scheduled to be visited on the stack.  Because a module is never
again checked for prior visits once it's scheduled for reloading, modules
that are rescheduled before they are reloaded will be reloaded redundantly.
Though uncaught in testing and somewhat dependent on ordering, this can 
happen anytime a module is imported by multiple others:
===========================================================================
"""

def transitive_reload0(modules, visited):           # 0: Original version
    while modules:
        next = modules.pop()                        # Delete next item at end
        status(next)                                # Reload this, push attrs
        tryreload(next)
        visited.add(next)
        modules.extend(x for x in next.__dict__.values()
            if type(x) == types.ModuleType and x not in visited)

def reload_all0(*modules):
    transitive_reload0(list(modules), set())


"""
===========================================================================
Your proposed solution works, because it checks the already-scheduled 
stack too, before extending it.  This prevents the reschedules, and 
hence the duplicate reloads:
===========================================================================
"""

def transitive_reload1(modules, visited):           # 1: Reader's working fix
    while modules:
        next = modules.pop()                        # Delete next item at end
        status(next)                                # Reload this, push attrs
        tryreload(next)
        visited.add(next)
        modules.extend(x for x in next.__dict__.values()
            if type(x) == types.ModuleType and x not in visited and x not in modules)

def reload_all1(*modules):
    transitive_reload1(list(modules), set())


"""
===========================================================================
If pressed, though, I'd say that it seems a bit gray to check for 
membership in a list while it is in the process of being extended.  
The generator passed to extend() yields one item to tack onto the 
list at a time - while also checking the contents of the list.  That
works, but seems very implicit (if not implementation dependent). 
I'd rather avoid the ambiguity and drama, and move the visited test 
up as follows, to trap repeats before their reload is attempted. 
This also _might_ be marginally faster because it trades list scans 
for set hashing, but benchmarking results are extra credit here:
===========================================================================
"""

def transitive_reload2(modules, visited):           # 2: Avoid 'in' during 'extend'
    while modules:
        next = modules.pop()                        # Delete next item at end
        if next in visited: continue                # Already reloaded anywhere?
        status(next)                                # Reload this, push attrs
        tryreload(next)
        visited.add(next)
        modules.extend(x for x in next.__dict__.values() 
            if type(x) == types.ModuleType)

def reload_all2(*modules):
    transitive_reload2(list(modules), set())


"""
===========================================================================
Better still: the following coding is much closer in structure to the 
recursive reloadall2 version that precedes it in the book, and hence 
better illustrates the real recursive-versus-stack point of this section.
It also traps non-module arguments at the top level; neither the original 
nor the two other alternatives above do.  Hence, this is how this example
will be patched in future reprints of the book (see the errata page):
===========================================================================
"""

def transitive_reload3(modules, visited):           # 3: Symmetry, catch non-mods
    while modules:
        next = modules.pop()                        # Delete next item at end
        if (type(next) == types.ModuleType          # Valid module object?
            and next not in visited):               # Not already reloaded?
            status(next)                            # Reload this, push attrs
            tryreload(next)
            visited.add(next)
            modules.extend(next.__dict__.values())

def reload_all3(*modules):
    transitive_reload3(list(modules), set())


"""
===========================================================================
When tested, all three recodings fully avoid the duplicate reloads 
and produce the same results, though visitation order varies slightly 
as expected (you can run all this code live by grabbing a copy from 
https://learning-python.com/reloadall3.py):
===========================================================================
"""

# prior API compatibility
reload_all = reload_all3

if __name__ == '__main__':
    # self-test
    import os, tkinter, reloadall3

    for test in (os, tkinter, reloadall3):
        print('\n%s\n[%s]' % ('-' * 40, test.__name__))

        for ra in (reload_all0, reload_all1, reload_all2, reload_all3):
            print('\n<%s>' % ra.__name__)
            ra(test)


"""
===========================================================================
RELATED NOTE: when this example is run today, Python 3.8 and 3.7 
generate a deprecation warning that:
 
   "the imp module is deprecated in favour of importlib"  

Alas, Python's module API has been a moving target for some time now 
(per the book, reload() used to be a built-in function in 2.X), and 
3.X has a now-long history of arbitrary and opinion-based changes like 
this that rudely break existing code.  

In this case, reload() has been pointlessly relocated  _twice_ in 3.X, 
and this example will probably also require changing "imp" to "importlib" 
in the near future to avoid failing altogether with an exception.  This 
is too much change to patch in reprints, so this note will have to suffice.

On the other hand, recursion coding concepts illustrated by this example
are still as relevant to the art of programming as ever.  Small details 
like module names do morph, but the larger fundamentals presented by 
this book don't.
===========================================================================
"""



[Home page] Books Code Blog Python Author Train Find ©M.Lutz