diff --git a/CHANGES.rst b/CHANGES.rst index f3853b5..1bc8d42 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,8 @@ Changelog Breaking changes: -- *add item here* +- Add support for Python 3.12. Drop support for Python 2 + [pbauer] New features: diff --git a/plone/reload/browser.py b/plone/reload/browser.py index 3feee43..127d9b5 100644 --- a/plone/reload/browser.py +++ b/plone/reload/browser.py @@ -15,22 +15,21 @@ @implementer(IReload) class Reload(BrowserView): - """Reload view. - """ + """Reload view.""" def __init__(self, context, request): BrowserView.__init__(self, context, request) self.message = None def __call__(self): - action = self.request.form.get('action') + action = self.request.form.get("action") if action is not None: if self.available(): - if action == 'code': + if action == "code": self.message = self.code_reload() - elif action == 'zcml': + elif action == "zcml": self.message = self.zcml_reload() - if action == 'template': + if action == "template": self.message = self.template_reload() return self.index() @@ -49,9 +48,9 @@ def template_reload(self): if HAS_CMF: reloaded = reload_template(self.context) if reloaded > 0: - return '%s templates reloaded.' % reloaded - return 'No templates reloaded.' - return 'CMF is not installed. Templates cannot be reloaded.' + return "%s templates reloaded." % reloaded + return "No templates reloaded." + return "CMF is not installed. Templates cannot be reloaded." def code_reload(self): if not self.available(): @@ -59,12 +58,12 @@ def code_reload(self): reloaded = reload_code() - result = '' + result = "" if reloaded: - result += 'Code reloaded:\n\n' - result += '\n'.join(reloaded) + result += "Code reloaded:\n\n" + result += "\n".join(reloaded) else: - result = 'No code reloaded!' + result = "No code reloaded!" return result def zcml_reload(self): @@ -79,11 +78,11 @@ def zcml_reload(self): # TODO Minimize all caches, we only really want to invalidate the # local site manager from all caches self.context._p_jar.db().cacheMinimize() - result = '' + result = "" if reloaded: - result += 'Code reloaded:\n\n' - result += '\n'.join(reloaded) + result += "Code reloaded:\n\n" + result += "\n".join(reloaded) else: - result = 'No code reloaded!' - result += '\n\nGlobal ZCML reloaded.' + result = "No code reloaded!" + result += "\n\nGlobal ZCML reloaded." return result diff --git a/plone/reload/code.py b/plone/reload/code.py index 14ee521..fefdefa 100644 --- a/plone/reload/code.py +++ b/plone/reload/code.py @@ -1,67 +1,43 @@ -import os -import sys - -from os.path import abspath -from os.path import isfile - +from importlib.util import cache_from_source, source_from_cache from plone.reload import config from plone.reload.xreload import Reloader +import os +import sys + _marker = object() MOD_TIMES = dict() -try: - # Py3 - from imp import cache_from_source, source_from_cache - - def _cache_from_source(path): - if '__pycache__' in path: - return path - return cache_from_source(path) - - def _source_from_cache(path): - if '__pycache__' in path: - return source_from_cache(path) +def _cache_from_source(path): + if "__pycache__" in path: return path + return cache_from_source(path) -except ImportError: - # Py2 - def _cache_from_source(path): - if path.endswith('pyc') or path.endswith('pyo'): - cache = path - else: - cache = path + 'c' - if os.path.isfile(cache): - path = cache - return path - def _source_from_cache(path): - source = path - if path.endswith('pyc') or path.endswith('pyo'): - source = path[:-1] - if os.path.isfile(source): - path = source - return path +def _source_from_cache(path): + if "__pycache__" in path: + return source_from_cache(path) + return path def in_search_path(path): - if 'site-packages' in path: + if "site-packages" in path: return False - elif '.egg' in path: + elif ".egg" in path: return False return True def search_modules(): modules = [] - for name, module in sys.modules.items(): + for _, module in sys.modules.items(): if module is not None: - f = getattr(module, '__file__', None) + f = getattr(module, "__file__", None) # Standard library modules don't have a __file__ if f is None: continue - f = abspath(_source_from_cache(f)) + f = os.path.abspath(_source_from_cache(f)) if config.EXCLUDE_SITE_PACKAGES: if in_search_path(f): modules.append((f, module)) @@ -75,7 +51,7 @@ def get_mod_time(path): # If we have the compiled source, look for the source code change date path = _source_from_cache(path) # protect against missing and unaccessible files - if isfile(path): + if os.path.isfile(path): mtime = os.stat(path)[8] return mtime diff --git a/plone/reload/interfaces.py b/plone/reload/interfaces.py index 839c7b9..3f401c7 100644 --- a/plone/reload/interfaces.py +++ b/plone/reload/interfaces.py @@ -2,8 +2,7 @@ class IReload(Interface): - """Interface for the ZCML reload view. - """ + """Interface for the ZCML reload view.""" def status(): """Return a status text.""" diff --git a/plone/reload/template.py b/plone/reload/template.py index 73873d1..fe28c58 100644 --- a/plone/reload/template.py +++ b/plone/reload/template.py @@ -8,7 +8,7 @@ def reload_skins(tool): for folder in tool.objectValues(): for obj in folder.objectValues(): if isinstance(obj, FSObject): - parsed = getattr(obj, '_parsed', 0) + parsed = getattr(obj, "_parsed", 0) if parsed: obj._parsed = 0 counter += 1 @@ -19,7 +19,7 @@ def reload_template(root): counter = 0 for obj in root.objectValues(): if ISiteRoot.providedBy(obj): - tool = getToolByName(obj, 'portal_skins', None) + tool = getToolByName(obj, "portal_skins", None) if tool is not None: counter = reload_skins(tool) return counter diff --git a/plone/reload/xreload.py b/plone/reload/xreload.py index 4b5a56c..c97c977 100644 --- a/plone/reload/xreload.py +++ b/plone/reload/xreload.py @@ -12,16 +12,13 @@ """ -import marshal -import imp +from importlib import reload +from zope.interface.interface import Specification + +import importlib +import inspect import sys import types -import inspect - -import six -from six.moves import reload_module -import zope.component - CLASS_STATICS = frozenset(["__dict__", "__doc__", "__module__", "__weakref__"]) @@ -53,45 +50,37 @@ def reload(self): # 'whatever' i = modname.rfind(".") if i >= 0: - pkgname, modname = modname[:i], modname[i + 1:] + pkgname, modname = modname[:i], modname[i + 1 :] else: pkgname = None # Compute the search path if pkgname: # We're not reloading the package, only the module in it pkg = sys.modules[pkgname] - path = pkg.__path__ # Search inside the package else: # Search the top-level module path pkg = None - path = None # Make find_module() uses the default search path - # Find the module; may raise ImportError - (stream, filename, (suffix, mode, kind)) = imp.find_module( - modname, path) - # Turn it into a code object - try: - # Is it Python source code or byte code read from a file? - # XXX Could handle frozen modules, zip-import modules - if kind not in (imp.PY_COMPILED, imp.PY_SOURCE): - # Fall back to built-in reload() - return reload_module(self.mod) - if kind == imp.PY_SOURCE: + package_name = pkg.__name__ if pkg else None + specs = importlib.util.find_spec(self.mod.__name__, package=package_name) + filename = specs.origin + if specs.has_location: + with open(filename, "rb") as stream: source = stream.read() # PeterB: if we don't strip the source code and add newline we # get a SyntaxError even if `python $filename` is perfectly # happy. - source = source.strip() + '\n' + source = source.strip() + b"\n" code = compile(source, filename, "exec") - else: - # I have no idea how to test this one - code = marshal.load(stream) # pragma NO COVER - finally: - if stream: - stream.close() + else: + # Fall back to built-in reload() + return reload(self.mod) + # Execute the code im a temporary namespace; if this fails, no changes - tmpns = {'__name__': '%s.%s' % (pkgname, modname), - '__file__': filename, - '__doc__': modns['__doc__']} + tmpns = { + "__name__": "%s.%s" % (pkgname, modname), + "__file__": filename, + "__doc__": modns["__doc__"], + } exec(code, tmpns) # Now we get to the hard part _update_scope(modns, tmpns) @@ -117,13 +106,13 @@ def _update(self, oldobj, newobj): # Cop-out: if the type changed, give up return newobj - new_module = getattr(newobj, '__module__', None) + new_module = getattr(newobj, "__module__", None) if new_module != self.mod.__name__: # Do not update objects in-place that have been imported. # Just update their references. return newobj - if isinstance(newobj, zope.interface.interface.Specification): + if isinstance(newobj, Specification): # XXX we can't update interfaces because their internal # data structures break. We'll have to implement the reload method # for those and patch it in. @@ -162,19 +151,17 @@ def _update_scope(oldscope, newscope): oldscope[name] = newscope[name] # Delete names that are no longer current for name in oldnames - newnames: - if not name.startswith('__'): + if not name.startswith("__"): del oldscope[name] def _update_function(oldfunc, newfunc): """Update a function object.""" - if _closure_changed(six.get_function_closure(oldfunc), - six.get_function_closure(newfunc)): + if _closure_changed(oldfunc.__closure__, newfunc.__closure__): raise ClosureChanged() - setattr(oldfunc, six._func_code, six.get_function_code(newfunc)) - setattr(oldfunc, six._func_defaults, six.get_function_defaults(newfunc)) - _update_scope(six.get_function_globals(oldfunc), - six.get_function_globals(newfunc)) + setattr(oldfunc, "__code__", newfunc.__code__) + setattr(oldfunc, "__defaults__", newfunc.__defaults__) + _update_scope(oldfunc.__globals__, newfunc.__globals__) # XXX What else? return oldfunc @@ -182,8 +169,7 @@ def _update_function(oldfunc, newfunc): def _update_method(oldmeth, newmeth): """Update a method object.""" # XXX What if im_func is not a function? - _update_function(six.get_unbound_function(oldmeth), - six.get_unbound_function(newmeth)) + _update_function(oldmeth, newmeth) return oldmeth @@ -212,7 +198,7 @@ def _update_class(oldclass, newclass): if isinstance(new, (types.FunctionType, types.MethodType)): if isinstance(old, property) and not isinstance(new, property): # Removing a decorator - setattr(oldclass, name, six.get_unbound_function(new)) + setattr(oldclass, name, new) elif isinstance(new, types.FunctionType): # Under Py3 there are only functions _update_function(old, new) @@ -230,6 +216,6 @@ def _update_class(oldclass, newclass): setattr(oldclass, name, new) except ClosureChanged: # If the closure changed, we need to replace the entire function - setattr(oldclass, name, six.get_unbound_function(new)) + setattr(oldclass, name, new) return oldclass diff --git a/plone/reload/zcml.py b/plone/reload/zcml.py index 445353b..a5e5062 100644 --- a/plone/reload/zcml.py +++ b/plone/reload/zcml.py @@ -1,34 +1,25 @@ from zope.component import getGlobalSiteManager +from zope.component.hooks import setSite from zope.testing import cleanup - -try: - from zope.component.hooks import setSite -except ImportError: - from zope.site.hooks import setSite - - -# BBB: Zope 2.12 -try: - from Zope2.App import zcml - zcml # pyflakes -except ImportError: - from Products.Five import zcml - - -CORE_CLEANUPS = frozenset([ - 'OFS.metaconfigure', - 'Products.Five.zcml', - 'Products.Five.eventconfigure', - 'Products.Five.fiveconfigure', - 'Products.Five.sizeconfigure', - 'zope.component.globalregistry', - 'zope.component.hooks', - 'zope.schema.vocabulary', - 'zope.security.management', - 'zope.security.checker', - 'zope.site.hooks', - 'Zope2.App.zcml', -]) +from Zope2.App import zcml + + +CORE_CLEANUPS = frozenset( + [ + "OFS.metaconfigure", + "Products.Five.zcml", + "Products.Five.eventconfigure", + "Products.Five.fiveconfigure", + "Products.Five.sizeconfigure", + "zope.component.globalregistry", + "zope.component.hooks", + "zope.schema.vocabulary", + "zope.security.management", + "zope.security.checker", + "zope.site.hooks", + "Zope2.App.zcml", + ] +) def cleanups(): diff --git a/setup.py b/setup.py index c6c6e4f..201c9b9 100644 --- a/setup.py +++ b/setup.py @@ -1,52 +1,52 @@ from setuptools import setup -version = '3.0.3.dev0' +version = "3.0.3.dev0" setup( - name='plone.reload', + name="plone.reload", version=version, description="Configuration and code reload without server restarts.", - long_description=(open('README.rst').read() + '\n' + - open('CHANGES.rst').read()), + long_description=(open("README.rst").read() + "\n" + open("CHANGES.rst").read()), classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Plone", "Framework :: Zope2", - "Framework :: Zope :: 4", + "Framework :: Zope :: 5", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", ], - keywords='zope plone reload', - author='Hanno Schlichting', - author_email='hanno@hannosch.eu', - url='https://pypi.org/project/plone.reload', - license='BSD', - packages=['plone', 'plone.reload'], - namespace_packages=['plone'], + keywords="zope plone reload", + author="Hanno Schlichting", + author_email="hanno@hannosch.eu", + url="https://pypi.org/project/plone.reload", + license="BSD", + packages=["plone", "plone.reload"], + namespace_packages=["plone"], include_package_data=True, zip_safe=False, install_requires=[ - 'setuptools', - 'six', - 'zope.component', - 'zope.interface', - 'zope.processlifetime', - 'zope.publisher', - 'zope.site', - 'zope.testing', - 'Zope2 >= 2.13', + "setuptools", + "zope.component", + "zope.interface", + "zope.processlifetime", + "zope.publisher", + "zope.site", + "zope.testing", + "Zope2 >= 2.13", ], extras_require=dict( - cmf=['Products.CMFCore'], + cmf=["Products.CMFCore"], ), entry_points=""" [z3c.autoinclude.plugin]