diff --git a/lib/importlib_metadata/__init__.py b/lib/importlib_metadata/__init__.py
new file mode 100644
index 00000000..5ac8be23
--- /dev/null
+++ b/lib/importlib_metadata/__init__.py
@@ -0,0 +1,1075 @@
+import os
+import re
+import abc
+import csv
+import sys
+import zipp
+import email
+import pathlib
+import operator
+import textwrap
+import warnings
+import functools
+import itertools
+import posixpath
+import collections
+
+from . import _adapters, _meta
+from ._collections import FreezableDefaultDict, Pair
+from ._compat import (
+ NullFinder,
+ install,
+ pypy_partial,
+)
+from ._functools import method_cache, pass_none
+from ._itertools import always_iterable, unique_everseen
+from ._meta import PackageMetadata, SimplePath
+
+from contextlib import suppress
+from importlib import import_module
+from importlib.abc import MetaPathFinder
+from itertools import starmap
+from typing import List, Mapping, Optional, Union
+
+
+__all__ = [
+ 'Distribution',
+ 'DistributionFinder',
+ 'PackageMetadata',
+ 'PackageNotFoundError',
+ 'distribution',
+ 'distributions',
+ 'entry_points',
+ 'files',
+ 'metadata',
+ 'packages_distributions',
+ 'requires',
+ 'version',
+]
+
+
+class PackageNotFoundError(ModuleNotFoundError):
+ """The package was not found."""
+
+ def __str__(self):
+ return f"No package metadata was found for {self.name}"
+
+ @property
+ def name(self):
+ (name,) = self.args
+ return name
+
+
+class Sectioned:
+ """
+ A simple entry point config parser for performance
+
+ >>> for item in Sectioned.read(Sectioned._sample):
+ ... print(item)
+ Pair(name='sec1', value='# comments ignored')
+ Pair(name='sec1', value='a = 1')
+ Pair(name='sec1', value='b = 2')
+ Pair(name='sec2', value='a = 2')
+
+ >>> res = Sectioned.section_pairs(Sectioned._sample)
+ >>> item = next(res)
+ >>> item.name
+ 'sec1'
+ >>> item.value
+ Pair(name='a', value='1')
+ >>> item = next(res)
+ >>> item.value
+ Pair(name='b', value='2')
+ >>> item = next(res)
+ >>> item.name
+ 'sec2'
+ >>> item.value
+ Pair(name='a', value='2')
+ >>> list(res)
+ []
+ """
+
+ _sample = textwrap.dedent(
+ """
+ [sec1]
+ # comments ignored
+ a = 1
+ b = 2
+
+ [sec2]
+ a = 2
+ """
+ ).lstrip()
+
+ @classmethod
+ def section_pairs(cls, text):
+ return (
+ section._replace(value=Pair.parse(section.value))
+ for section in cls.read(text, filter_=cls.valid)
+ if section.name is not None
+ )
+
+ @staticmethod
+ def read(text, filter_=None):
+ lines = filter(filter_, map(str.strip, text.splitlines()))
+ name = None
+ for value in lines:
+ section_match = value.startswith('[') and value.endswith(']')
+ if section_match:
+ name = value.strip('[]')
+ continue
+ yield Pair(name, value)
+
+ @staticmethod
+ def valid(line):
+ return line and not line.startswith('#')
+
+
+class DeprecatedTuple:
+ """
+ Provide subscript item access for backward compatibility.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> ep = EntryPoint(name='name', value='value', group='group')
+ >>> ep[:]
+ ('name', 'value', 'group')
+ >>> ep[0]
+ 'name'
+ >>> len(recwarn)
+ 1
+ """
+
+ _warn = functools.partial(
+ warnings.warn,
+ "EntryPoint tuple interface is deprecated. Access members by name.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def __getitem__(self, item):
+ self._warn()
+ return self._key()[item]
+
+
+class EntryPoint(DeprecatedTuple):
+ """An entry point as defined by Python packaging conventions.
+
+ See `the packaging docs on entry points
+ `_
+ for more information.
+
+ >>> ep = EntryPoint(
+ ... name=None, group=None, value='package.module:attr [extra1, extra2]')
+ >>> ep.module
+ 'package.module'
+ >>> ep.attr
+ 'attr'
+ >>> ep.extras
+ ['extra1', 'extra2']
+ """
+
+ pattern = re.compile(
+ r'(?P[\w.]+)\s*'
+ r'(:\s*(?P[\w.]+)\s*)?'
+ r'((?P\[.*\])\s*)?$'
+ )
+ """
+ A regular expression describing the syntax for an entry point,
+ which might look like:
+
+ - module
+ - package.module
+ - package.module:attribute
+ - package.module:object.attribute
+ - package.module:attr [extra1, extra2]
+
+ Other combinations are possible as well.
+
+ The expression is lenient about whitespace around the ':',
+ following the attr, and following any extras.
+ """
+
+ dist: Optional['Distribution'] = None
+
+ def __init__(self, name, value, group):
+ vars(self).update(name=name, value=value, group=group)
+
+ def load(self):
+ """Load the entry point from its definition. If only a module
+ is indicated by the value, return that module. Otherwise,
+ return the named object.
+ """
+ match = self.pattern.match(self.value)
+ module = import_module(match.group('module'))
+ attrs = filter(None, (match.group('attr') or '').split('.'))
+ return functools.reduce(getattr, attrs, module)
+
+ @property
+ def module(self):
+ match = self.pattern.match(self.value)
+ return match.group('module')
+
+ @property
+ def attr(self):
+ match = self.pattern.match(self.value)
+ return match.group('attr')
+
+ @property
+ def extras(self):
+ match = self.pattern.match(self.value)
+ return re.findall(r'\w+', match.group('extras') or '')
+
+ def _for(self, dist):
+ vars(self).update(dist=dist)
+ return self
+
+ def __iter__(self):
+ """
+ Supply iter so one may construct dicts of EntryPoints by name.
+ """
+ msg = (
+ "Construction of dict of EntryPoints is deprecated in "
+ "favor of EntryPoints."
+ )
+ warnings.warn(msg, DeprecationWarning)
+ return iter((self.name, self))
+
+ def matches(self, **params):
+ """
+ EntryPoint matches the given parameters.
+
+ >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]')
+ >>> ep.matches(group='foo')
+ True
+ >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]')
+ True
+ >>> ep.matches(group='foo', name='other')
+ False
+ >>> ep.matches()
+ True
+ >>> ep.matches(extras=['extra1', 'extra2'])
+ True
+ >>> ep.matches(module='bing')
+ True
+ >>> ep.matches(attr='bong')
+ True
+ """
+ attrs = (getattr(self, param) for param in params)
+ return all(map(operator.eq, params.values(), attrs))
+
+ def _key(self):
+ return self.name, self.value, self.group
+
+ def __lt__(self, other):
+ return self._key() < other._key()
+
+ def __eq__(self, other):
+ return self._key() == other._key()
+
+ def __setattr__(self, name, value):
+ raise AttributeError("EntryPoint objects are immutable.")
+
+ def __repr__(self):
+ return (
+ f'EntryPoint(name={self.name!r}, value={self.value!r}, '
+ f'group={self.group!r})'
+ )
+
+ def __hash__(self):
+ return hash(self._key())
+
+
+class DeprecatedList(list):
+ """
+ Allow an otherwise immutable object to implement mutability
+ for compatibility.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> dl = DeprecatedList(range(3))
+ >>> dl[0] = 1
+ >>> dl.append(3)
+ >>> del dl[3]
+ >>> dl.reverse()
+ >>> dl.sort()
+ >>> dl.extend([4])
+ >>> dl.pop(-1)
+ 4
+ >>> dl.remove(1)
+ >>> dl += [5]
+ >>> dl + [6]
+ [1, 2, 5, 6]
+ >>> dl + (6,)
+ [1, 2, 5, 6]
+ >>> dl.insert(0, 0)
+ >>> dl
+ [0, 1, 2, 5]
+ >>> dl == [0, 1, 2, 5]
+ True
+ >>> dl == (0, 1, 2, 5)
+ True
+ >>> len(recwarn)
+ 1
+ """
+
+ __slots__ = ()
+
+ _warn = functools.partial(
+ warnings.warn,
+ "EntryPoints list interface is deprecated. Cast to list if needed.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def _wrap_deprecated_method(method_name: str): # type: ignore
+ def wrapped(self, *args, **kwargs):
+ self._warn()
+ return getattr(super(), method_name)(*args, **kwargs)
+
+ return method_name, wrapped
+
+ locals().update(
+ map(
+ _wrap_deprecated_method,
+ '__setitem__ __delitem__ append reverse extend pop remove '
+ '__iadd__ insert sort'.split(),
+ )
+ )
+
+ def __add__(self, other):
+ if not isinstance(other, tuple):
+ self._warn()
+ other = tuple(other)
+ return self.__class__(tuple(self) + other)
+
+ def __eq__(self, other):
+ if not isinstance(other, tuple):
+ self._warn()
+ other = tuple(other)
+
+ return tuple(self).__eq__(other)
+
+
+class EntryPoints(DeprecatedList):
+ """
+ An immutable collection of selectable EntryPoint objects.
+ """
+
+ __slots__ = ()
+
+ def __getitem__(self, name): # -> EntryPoint:
+ """
+ Get the EntryPoint in self matching name.
+ """
+ if isinstance(name, int):
+ warnings.warn(
+ "Accessing entry points by index is deprecated. "
+ "Cast to tuple if needed.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return super().__getitem__(name)
+ try:
+ return next(iter(self.select(name=name)))
+ except StopIteration:
+ raise KeyError(name)
+
+ def select(self, **params):
+ """
+ Select entry points from self that match the
+ given parameters (typically group and/or name).
+ """
+ return EntryPoints(ep for ep in self if ep.matches(**params))
+
+ @property
+ def names(self):
+ """
+ Return the set of all names of all entry points.
+ """
+ return {ep.name for ep in self}
+
+ @property
+ def groups(self):
+ """
+ Return the set of all groups of all entry points.
+
+ For coverage while SelectableGroups is present.
+ >>> EntryPoints().groups
+ set()
+ """
+ return {ep.group for ep in self}
+
+ @classmethod
+ def _from_text_for(cls, text, dist):
+ return cls(ep._for(dist) for ep in cls._from_text(text))
+
+ @staticmethod
+ def _from_text(text):
+ return (
+ EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
+ for item in Sectioned.section_pairs(text or '')
+ )
+
+
+class Deprecated:
+ """
+ Compatibility add-in for mapping to indicate that
+ mapping behavior is deprecated.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> class DeprecatedDict(Deprecated, dict): pass
+ >>> dd = DeprecatedDict(foo='bar')
+ >>> dd.get('baz', None)
+ >>> dd['foo']
+ 'bar'
+ >>> list(dd)
+ ['foo']
+ >>> list(dd.keys())
+ ['foo']
+ >>> 'foo' in dd
+ True
+ >>> list(dd.values())
+ ['bar']
+ >>> len(recwarn)
+ 1
+ """
+
+ _warn = functools.partial(
+ warnings.warn,
+ "SelectableGroups dict interface is deprecated. Use select.",
+ DeprecationWarning,
+ stacklevel=pypy_partial(2),
+ )
+
+ def __getitem__(self, name):
+ self._warn()
+ return super().__getitem__(name)
+
+ def get(self, name, default=None):
+ self._warn()
+ return super().get(name, default)
+
+ def __iter__(self):
+ self._warn()
+ return super().__iter__()
+
+ def __contains__(self, *args):
+ self._warn()
+ return super().__contains__(*args)
+
+ def keys(self):
+ self._warn()
+ return super().keys()
+
+ def values(self):
+ self._warn()
+ return super().values()
+
+
+class SelectableGroups(Deprecated, dict):
+ """
+ A backward- and forward-compatible result from
+ entry_points that fully implements the dict interface.
+ """
+
+ @classmethod
+ def load(cls, eps):
+ by_group = operator.attrgetter('group')
+ ordered = sorted(eps, key=by_group)
+ grouped = itertools.groupby(ordered, by_group)
+ return cls((group, EntryPoints(eps)) for group, eps in grouped)
+
+ @property
+ def _all(self):
+ """
+ Reconstruct a list of all entrypoints from the groups.
+ """
+ groups = super(Deprecated, self).values()
+ return EntryPoints(itertools.chain.from_iterable(groups))
+
+ @property
+ def groups(self):
+ return self._all.groups
+
+ @property
+ def names(self):
+ """
+ for coverage:
+ >>> SelectableGroups().names
+ set()
+ """
+ return self._all.names
+
+ def select(self, **params):
+ if not params:
+ return self
+ return self._all.select(**params)
+
+
+class PackagePath(pathlib.PurePosixPath):
+ """A reference to a path in a package"""
+
+ def read_text(self, encoding='utf-8'):
+ with self.locate().open(encoding=encoding) as stream:
+ return stream.read()
+
+ def read_binary(self):
+ with self.locate().open('rb') as stream:
+ return stream.read()
+
+ def locate(self):
+ """Return a path-like object for this path"""
+ return self.dist.locate_file(self)
+
+
+class FileHash:
+ def __init__(self, spec):
+ self.mode, _, self.value = spec.partition('=')
+
+ def __repr__(self):
+ return f''
+
+
+class Distribution:
+ """A Python distribution package."""
+
+ @abc.abstractmethod
+ def read_text(self, filename):
+ """Attempt to load metadata file given by the name.
+
+ :param filename: The name of the file in the distribution info.
+ :return: The text if found, otherwise None.
+ """
+
+ @abc.abstractmethod
+ def locate_file(self, path):
+ """
+ Given a path to a file in this distribution, return a path
+ to it.
+ """
+
+ @classmethod
+ def from_name(cls, name):
+ """Return the Distribution for the given package name.
+
+ :param name: The name of the distribution package to search for.
+ :return: The Distribution instance (or subclass thereof) for the named
+ package, if found.
+ :raises PackageNotFoundError: When the named package's distribution
+ metadata cannot be found.
+ """
+ for resolver in cls._discover_resolvers():
+ dists = resolver(DistributionFinder.Context(name=name))
+ dist = next(iter(dists), None)
+ if dist is not None:
+ return dist
+ else:
+ raise PackageNotFoundError(name)
+
+ @classmethod
+ def discover(cls, **kwargs):
+ """Return an iterable of Distribution objects for all packages.
+
+ Pass a ``context`` or pass keyword arguments for constructing
+ a context.
+
+ :context: A ``DistributionFinder.Context`` object.
+ :return: Iterable of Distribution objects for all packages.
+ """
+ context = kwargs.pop('context', None)
+ if context and kwargs:
+ raise ValueError("cannot accept context and kwargs")
+ context = context or DistributionFinder.Context(**kwargs)
+ return itertools.chain.from_iterable(
+ resolver(context) for resolver in cls._discover_resolvers()
+ )
+
+ @staticmethod
+ def at(path):
+ """Return a Distribution for the indicated metadata path
+
+ :param path: a string or path-like object
+ :return: a concrete Distribution instance for the path
+ """
+ return PathDistribution(pathlib.Path(path))
+
+ @staticmethod
+ def _discover_resolvers():
+ """Search the meta_path for resolvers."""
+ declared = (
+ getattr(finder, 'find_distributions', None) for finder in sys.meta_path
+ )
+ return filter(None, declared)
+
+ @property
+ def metadata(self) -> _meta.PackageMetadata:
+ """Return the parsed metadata for this Distribution.
+
+ The returned object will have keys that name the various bits of
+ metadata. See PEP 566 for details.
+ """
+ text = (
+ self.read_text('METADATA')
+ or self.read_text('PKG-INFO')
+ # This last clause is here to support old egg-info files. Its
+ # effect is to just end up using the PathDistribution's self._path
+ # (which points to the egg-info file) attribute unchanged.
+ or self.read_text('')
+ )
+ return _adapters.Message(email.message_from_string(text))
+
+ @property
+ def name(self):
+ """Return the 'Name' metadata for the distribution package."""
+ return self.metadata['Name']
+
+ @property
+ def _normalized_name(self):
+ """Return a normalized version of the name."""
+ return Prepared.normalize(self.name)
+
+ @property
+ def version(self):
+ """Return the 'Version' metadata for the distribution package."""
+ return self.metadata['Version']
+
+ @property
+ def entry_points(self):
+ return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
+
+ @property
+ def files(self):
+ """Files in this distribution.
+
+ :return: List of PackagePath for this distribution or None
+
+ Result is `None` if the metadata file that enumerates files
+ (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
+ missing.
+ Result may be empty if the metadata exists but is empty.
+ """
+
+ def make_file(name, hash=None, size_str=None):
+ result = PackagePath(name)
+ result.hash = FileHash(hash) if hash else None
+ result.size = int(size_str) if size_str else None
+ result.dist = self
+ return result
+
+ @pass_none
+ def make_files(lines):
+ return list(starmap(make_file, csv.reader(lines)))
+
+ return make_files(self._read_files_distinfo() or self._read_files_egginfo())
+
+ def _read_files_distinfo(self):
+ """
+ Read the lines of RECORD
+ """
+ text = self.read_text('RECORD')
+ return text and text.splitlines()
+
+ def _read_files_egginfo(self):
+ """
+ SOURCES.txt might contain literal commas, so wrap each line
+ in quotes.
+ """
+ text = self.read_text('SOURCES.txt')
+ return text and map('"{}"'.format, text.splitlines())
+
+ @property
+ def requires(self):
+ """Generated requirements specified for this Distribution"""
+ reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
+ return reqs and list(reqs)
+
+ def _read_dist_info_reqs(self):
+ return self.metadata.get_all('Requires-Dist')
+
+ def _read_egg_info_reqs(self):
+ source = self.read_text('requires.txt')
+ return pass_none(self._deps_from_requires_text)(source)
+
+ @classmethod
+ def _deps_from_requires_text(cls, source):
+ return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source))
+
+ @staticmethod
+ def _convert_egg_info_reqs_to_simple_reqs(sections):
+ """
+ Historically, setuptools would solicit and store 'extra'
+ requirements, including those with environment markers,
+ in separate sections. More modern tools expect each
+ dependency to be defined separately, with any relevant
+ extras and environment markers attached directly to that
+ requirement. This method converts the former to the
+ latter. See _test_deps_from_requires_text for an example.
+ """
+
+ def make_condition(name):
+ return name and f'extra == "{name}"'
+
+ def quoted_marker(section):
+ section = section or ''
+ extra, sep, markers = section.partition(':')
+ if extra and markers:
+ markers = f'({markers})'
+ conditions = list(filter(None, [markers, make_condition(extra)]))
+ return '; ' + ' and '.join(conditions) if conditions else ''
+
+ def url_req_space(req):
+ """
+ PEP 508 requires a space between the url_spec and the quoted_marker.
+ Ref python/importlib_metadata#357.
+ """
+ # '@' is uniquely indicative of a url_req.
+ return ' ' * ('@' in req)
+
+ for section in sections:
+ space = url_req_space(section.value)
+ yield section.value + space + quoted_marker(section.name)
+
+
+class DistributionFinder(MetaPathFinder):
+ """
+ A MetaPathFinder capable of discovering installed distributions.
+ """
+
+ class Context:
+ """
+ Keyword arguments presented by the caller to
+ ``distributions()`` or ``Distribution.discover()``
+ to narrow the scope of a search for distributions
+ in all DistributionFinders.
+
+ Each DistributionFinder may expect any parameters
+ and should attempt to honor the canonical
+ parameters defined below when appropriate.
+ """
+
+ name = None
+ """
+ Specific name for which a distribution finder should match.
+ A name of ``None`` matches all distributions.
+ """
+
+ def __init__(self, **kwargs):
+ vars(self).update(kwargs)
+
+ @property
+ def path(self):
+ """
+ The sequence of directory path that a distribution finder
+ should search.
+
+ Typically refers to Python installed package paths such as
+ "site-packages" directories and defaults to ``sys.path``.
+ """
+ return vars(self).get('path', sys.path)
+
+ @abc.abstractmethod
+ def find_distributions(self, context=Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching the ``context``,
+ a DistributionFinder.Context instance.
+ """
+
+
+class FastPath:
+ """
+ Micro-optimized class for searching a path for
+ children.
+
+ >>> FastPath('').children()
+ ['...']
+ """
+
+ @functools.lru_cache() # type: ignore
+ def __new__(cls, root):
+ return super().__new__(cls)
+
+ def __init__(self, root):
+ self.root = root
+
+ def joinpath(self, child):
+ return pathlib.Path(self.root, child)
+
+ def children(self):
+ with suppress(Exception):
+ return os.listdir(self.root or '.')
+ with suppress(Exception):
+ return self.zip_children()
+ return []
+
+ def zip_children(self):
+ zip_path = zipp.Path(self.root)
+ names = zip_path.root.namelist()
+ self.joinpath = zip_path.joinpath
+
+ return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
+
+ def search(self, name):
+ return self.lookup(self.mtime).search(name)
+
+ @property
+ def mtime(self):
+ with suppress(OSError):
+ return os.stat(self.root).st_mtime
+ self.lookup.cache_clear()
+
+ @method_cache
+ def lookup(self, mtime):
+ return Lookup(self)
+
+
+class Lookup:
+ def __init__(self, path: FastPath):
+ base = os.path.basename(path.root).lower()
+ base_is_egg = base.endswith(".egg")
+ self.infos = FreezableDefaultDict(list)
+ self.eggs = FreezableDefaultDict(list)
+
+ for child in path.children():
+ low = child.lower()
+ if low.endswith((".dist-info", ".egg-info")):
+ # rpartition is faster than splitext and suitable for this purpose.
+ name = low.rpartition(".")[0].partition("-")[0]
+ normalized = Prepared.normalize(name)
+ self.infos[normalized].append(path.joinpath(child))
+ elif base_is_egg and low == "egg-info":
+ name = base.rpartition(".")[0].partition("-")[0]
+ legacy_normalized = Prepared.legacy_normalize(name)
+ self.eggs[legacy_normalized].append(path.joinpath(child))
+
+ self.infos.freeze()
+ self.eggs.freeze()
+
+ def search(self, prepared):
+ infos = (
+ self.infos[prepared.normalized]
+ if prepared
+ else itertools.chain.from_iterable(self.infos.values())
+ )
+ eggs = (
+ self.eggs[prepared.legacy_normalized]
+ if prepared
+ else itertools.chain.from_iterable(self.eggs.values())
+ )
+ return itertools.chain(infos, eggs)
+
+
+class Prepared:
+ """
+ A prepared search for metadata on a possibly-named package.
+ """
+
+ normalized = None
+ legacy_normalized = None
+
+ def __init__(self, name):
+ self.name = name
+ if name is None:
+ return
+ self.normalized = self.normalize(name)
+ self.legacy_normalized = self.legacy_normalize(name)
+
+ @staticmethod
+ def normalize(name):
+ """
+ PEP 503 normalization plus dashes as underscores.
+ """
+ return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
+
+ @staticmethod
+ def legacy_normalize(name):
+ """
+ Normalize the package name as found in the convention in
+ older packaging tools versions and specs.
+ """
+ return name.lower().replace('-', '_')
+
+ def __bool__(self):
+ return bool(self.name)
+
+
+@install
+class MetadataPathFinder(NullFinder, DistributionFinder):
+ """A degenerate finder for distribution packages on the file system.
+
+ This finder supplies only a find_distributions() method for versions
+ of Python that do not have a PathFinder find_distributions().
+ """
+
+ def find_distributions(self, context=DistributionFinder.Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching ``context.name``
+ (or all names if ``None`` indicated) along the paths in the list
+ of directories ``context.path``.
+ """
+ found = self._search_paths(context.name, context.path)
+ return map(PathDistribution, found)
+
+ @classmethod
+ def _search_paths(cls, name, paths):
+ """Find metadata directories in paths heuristically."""
+ prepared = Prepared(name)
+ return itertools.chain.from_iterable(
+ path.search(prepared) for path in map(FastPath, paths)
+ )
+
+ def invalidate_caches(cls):
+ FastPath.__new__.cache_clear()
+
+
+class PathDistribution(Distribution):
+ def __init__(self, path: SimplePath):
+ """Construct a distribution.
+
+ :param path: SimplePath indicating the metadata directory.
+ """
+ self._path = path
+
+ def read_text(self, filename):
+ with suppress(
+ FileNotFoundError,
+ IsADirectoryError,
+ KeyError,
+ NotADirectoryError,
+ PermissionError,
+ ):
+ return self._path.joinpath(filename).read_text(encoding='utf-8')
+
+ read_text.__doc__ = Distribution.read_text.__doc__
+
+ def locate_file(self, path):
+ return self._path.parent / path
+
+ @property
+ def _normalized_name(self):
+ """
+ Performance optimization: where possible, resolve the
+ normalized name from the file system path.
+ """
+ stem = os.path.basename(str(self._path))
+ return self._name_from_stem(stem) or super()._normalized_name
+
+ def _name_from_stem(self, stem):
+ name, ext = os.path.splitext(stem)
+ if ext not in ('.dist-info', '.egg-info'):
+ return
+ name, sep, rest = stem.partition('-')
+ return name
+
+
+def distribution(distribution_name):
+ """Get the ``Distribution`` instance for the named package.
+
+ :param distribution_name: The name of the distribution package as a string.
+ :return: A ``Distribution`` instance (or subclass thereof).
+ """
+ return Distribution.from_name(distribution_name)
+
+
+def distributions(**kwargs):
+ """Get all ``Distribution`` instances in the current environment.
+
+ :return: An iterable of ``Distribution`` instances.
+ """
+ return Distribution.discover(**kwargs)
+
+
+def metadata(distribution_name) -> _meta.PackageMetadata:
+ """Get the metadata for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: A PackageMetadata containing the parsed metadata.
+ """
+ return Distribution.from_name(distribution_name).metadata
+
+
+def version(distribution_name):
+ """Get the version string for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: The version string for the package as defined in the package's
+ "Version" metadata key.
+ """
+ return distribution(distribution_name).version
+
+
+def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
+ """Return EntryPoint objects for all installed packages.
+
+ Pass selection parameters (group or name) to filter the
+ result to entry points matching those properties (see
+ EntryPoints.select()).
+
+ For compatibility, returns ``SelectableGroups`` object unless
+ selection parameters are supplied. In the future, this function
+ will return ``EntryPoints`` instead of ``SelectableGroups``
+ even when no selection parameters are supplied.
+
+ For maximum future compatibility, pass selection parameters
+ or invoke ``.select`` with parameters on the result.
+
+ :return: EntryPoints or SelectableGroups for all installed packages.
+ """
+ norm_name = operator.attrgetter('_normalized_name')
+ unique = functools.partial(unique_everseen, key=norm_name)
+ eps = itertools.chain.from_iterable(
+ dist.entry_points for dist in unique(distributions())
+ )
+ return SelectableGroups.load(eps).select(**params)
+
+
+def files(distribution_name):
+ """Return a list of files for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: List of files composing the distribution.
+ """
+ return distribution(distribution_name).files
+
+
+def requires(distribution_name):
+ """
+ Return a list of requirements for the named package.
+
+ :return: An iterator of requirements, suitable for
+ packaging.requirement.Requirement.
+ """
+ return distribution(distribution_name).requires
+
+
+def packages_distributions() -> Mapping[str, List[str]]:
+ """
+ Return a mapping of top-level packages to their
+ distributions.
+
+ >>> import collections.abc
+ >>> pkgs = packages_distributions()
+ >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
+ True
+ """
+ pkg_to_dist = collections.defaultdict(list)
+ for dist in distributions():
+ for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
+ pkg_to_dist[pkg].append(dist.metadata['Name'])
+ return dict(pkg_to_dist)
+
+
+def _top_level_declared(dist):
+ return (dist.read_text('top_level.txt') or '').split()
+
+
+def _top_level_inferred(dist):
+ return {
+ f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+ for f in always_iterable(dist.files)
+ if f.suffix == ".py"
+ }
diff --git a/lib/importlib_metadata/_adapters.py b/lib/importlib_metadata/_adapters.py
new file mode 100644
index 00000000..aa460d3e
--- /dev/null
+++ b/lib/importlib_metadata/_adapters.py
@@ -0,0 +1,68 @@
+import re
+import textwrap
+import email.message
+
+from ._text import FoldedCase
+
+
+class Message(email.message.Message):
+ multiple_use_keys = set(
+ map(
+ FoldedCase,
+ [
+ 'Classifier',
+ 'Obsoletes-Dist',
+ 'Platform',
+ 'Project-URL',
+ 'Provides-Dist',
+ 'Provides-Extra',
+ 'Requires-Dist',
+ 'Requires-External',
+ 'Supported-Platform',
+ 'Dynamic',
+ ],
+ )
+ )
+ """
+ Keys that may be indicated multiple times per PEP 566.
+ """
+
+ def __new__(cls, orig: email.message.Message):
+ res = super().__new__(cls)
+ vars(res).update(vars(orig))
+ return res
+
+ def __init__(self, *args, **kwargs):
+ self._headers = self._repair_headers()
+
+ # suppress spurious error from mypy
+ def __iter__(self):
+ return super().__iter__()
+
+ def _repair_headers(self):
+ def redent(value):
+ "Correct for RFC822 indentation"
+ if not value or '\n' not in value:
+ return value
+ return textwrap.dedent(' ' * 8 + value)
+
+ headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
+ if self._payload:
+ headers.append(('Description', self.get_payload()))
+ return headers
+
+ @property
+ def json(self):
+ """
+ Convert PackageMetadata to a JSON-compatible format
+ per PEP 0566.
+ """
+
+ def transform(key):
+ value = self.get_all(key) if key in self.multiple_use_keys else self[key]
+ if key == 'Keywords':
+ value = re.split(r'\s+', value)
+ tk = key.lower().replace('-', '_')
+ return tk, value
+
+ return dict(map(transform, map(FoldedCase, self)))
diff --git a/lib/importlib_metadata/_collections.py b/lib/importlib_metadata/_collections.py
new file mode 100644
index 00000000..cf0954e1
--- /dev/null
+++ b/lib/importlib_metadata/_collections.py
@@ -0,0 +1,30 @@
+import collections
+
+
+# from jaraco.collections 3.3
+class FreezableDefaultDict(collections.defaultdict):
+ """
+ Often it is desirable to prevent the mutation of
+ a default dict after its initial construction, such
+ as to prevent mutation during iteration.
+
+ >>> dd = FreezableDefaultDict(list)
+ >>> dd[0].append('1')
+ >>> dd.freeze()
+ >>> dd[1]
+ []
+ >>> len(dd)
+ 1
+ """
+
+ def __missing__(self, key):
+ return getattr(self, '_frozen', super().__missing__)(key)
+
+ def freeze(self):
+ self._frozen = lambda key: self.default_factory()
+
+
+class Pair(collections.namedtuple('Pair', 'name value')):
+ @classmethod
+ def parse(cls, text):
+ return cls(*map(str.strip, text.split("=", 1)))
diff --git a/lib/importlib_metadata/_compat.py b/lib/importlib_metadata/_compat.py
new file mode 100644
index 00000000..8fe4e4e3
--- /dev/null
+++ b/lib/importlib_metadata/_compat.py
@@ -0,0 +1,71 @@
+import sys
+import platform
+
+
+__all__ = ['install', 'NullFinder', 'Protocol']
+
+
+try:
+ from typing import Protocol
+except ImportError: # pragma: no cover
+ from typing_extensions import Protocol # type: ignore
+
+
+def install(cls):
+ """
+ Class decorator for installation on sys.meta_path.
+
+ Adds the backport DistributionFinder to sys.meta_path and
+ attempts to disable the finder functionality of the stdlib
+ DistributionFinder.
+ """
+ sys.meta_path.append(cls())
+ disable_stdlib_finder()
+ return cls
+
+
+def disable_stdlib_finder():
+ """
+ Give the backport primacy for discovering path-based distributions
+ by monkey-patching the stdlib O_O.
+
+ See #91 for more background for rationale on this sketchy
+ behavior.
+ """
+
+ def matches(finder):
+ return getattr(
+ finder, '__module__', None
+ ) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions')
+
+ for finder in filter(matches, sys.meta_path): # pragma: nocover
+ del finder.find_distributions
+
+
+class NullFinder:
+ """
+ A "Finder" (aka "MetaClassFinder") that never finds any modules,
+ but may find distributions.
+ """
+
+ @staticmethod
+ def find_spec(*args, **kwargs):
+ return None
+
+ # In Python 2, the import system requires finders
+ # to have a find_module() method, but this usage
+ # is deprecated in Python 3 in favor of find_spec().
+ # For the purposes of this finder (i.e. being present
+ # on sys.meta_path but having no other import
+ # system functionality), the two methods are identical.
+ find_module = find_spec
+
+
+def pypy_partial(val):
+ """
+ Adjust for variable stacklevel on partial under PyPy.
+
+ Workaround for #327.
+ """
+ is_pypy = platform.python_implementation() == 'PyPy'
+ return val + is_pypy
diff --git a/lib/importlib_metadata/_functools.py b/lib/importlib_metadata/_functools.py
new file mode 100644
index 00000000..71f66bd0
--- /dev/null
+++ b/lib/importlib_metadata/_functools.py
@@ -0,0 +1,104 @@
+import types
+import functools
+
+
+# from jaraco.functools 3.3
+def method_cache(method, cache_wrapper=None):
+ """
+ Wrap lru_cache to support storing the cache data in the object instances.
+
+ Abstracts the common paradigm where the method explicitly saves an
+ underscore-prefixed protected property on first call and returns that
+ subsequently.
+
+ >>> class MyClass:
+ ... calls = 0
+ ...
+ ... @method_cache
+ ... def method(self, value):
+ ... self.calls += 1
+ ... return value
+
+ >>> a = MyClass()
+ >>> a.method(3)
+ 3
+ >>> for x in range(75):
+ ... res = a.method(x)
+ >>> a.calls
+ 75
+
+ Note that the apparent behavior will be exactly like that of lru_cache
+ except that the cache is stored on each instance, so values in one
+ instance will not flush values from another, and when an instance is
+ deleted, so are the cached values for that instance.
+
+ >>> b = MyClass()
+ >>> for x in range(35):
+ ... res = b.method(x)
+ >>> b.calls
+ 35
+ >>> a.method(0)
+ 0
+ >>> a.calls
+ 75
+
+ Note that if method had been decorated with ``functools.lru_cache()``,
+ a.calls would have been 76 (due to the cached value of 0 having been
+ flushed by the 'b' instance).
+
+ Clear the cache with ``.cache_clear()``
+
+ >>> a.method.cache_clear()
+
+ Same for a method that hasn't yet been called.
+
+ >>> c = MyClass()
+ >>> c.method.cache_clear()
+
+ Another cache wrapper may be supplied:
+
+ >>> cache = functools.lru_cache(maxsize=2)
+ >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache)
+ >>> a = MyClass()
+ >>> a.method2()
+ 3
+
+ Caution - do not subsequently wrap the method with another decorator, such
+ as ``@property``, which changes the semantics of the function.
+
+ See also
+ http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
+ for another implementation and additional justification.
+ """
+ cache_wrapper = cache_wrapper or functools.lru_cache()
+
+ def wrapper(self, *args, **kwargs):
+ # it's the first call, replace the method with a cached, bound method
+ bound_method = types.MethodType(method, self)
+ cached_method = cache_wrapper(bound_method)
+ setattr(self, method.__name__, cached_method)
+ return cached_method(*args, **kwargs)
+
+ # Support cache clear even before cache has been created.
+ wrapper.cache_clear = lambda: None
+
+ return wrapper
+
+
+# From jaraco.functools 3.3
+def pass_none(func):
+ """
+ Wrap func so it's not called if its first param is None
+
+ >>> print_text = pass_none(print)
+ >>> print_text('text')
+ text
+ >>> print_text(None)
+ """
+
+ @functools.wraps(func)
+ def wrapper(param, *args, **kwargs):
+ if param is not None:
+ return func(param, *args, **kwargs)
+
+ return wrapper
diff --git a/lib/importlib_metadata/_itertools.py b/lib/importlib_metadata/_itertools.py
new file mode 100644
index 00000000..d4ca9b91
--- /dev/null
+++ b/lib/importlib_metadata/_itertools.py
@@ -0,0 +1,73 @@
+from itertools import filterfalse
+
+
+def unique_everseen(iterable, key=None):
+ "List unique elements, preserving order. Remember all elements ever seen."
+ # unique_everseen('AAAABBBCCDAABBB') --> A B C D
+ # unique_everseen('ABBCcAD', str.lower) --> A B C D
+ seen = set()
+ seen_add = seen.add
+ if key is None:
+ for element in filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
+ else:
+ for element in iterable:
+ k = key(element)
+ if k not in seen:
+ seen_add(k)
+ yield element
+
+
+# copied from more_itertools 8.8
+def always_iterable(obj, base_type=(str, bytes)):
+ """If *obj* is iterable, return an iterator over its items::
+
+ >>> obj = (1, 2, 3)
+ >>> list(always_iterable(obj))
+ [1, 2, 3]
+
+ If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+ >>> obj = 1
+ >>> list(always_iterable(obj))
+ [1]
+
+ If *obj* is ``None``, return an empty iterable:
+
+ >>> obj = None
+ >>> list(always_iterable(None))
+ []
+
+ By default, binary and text strings are not considered iterable::
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj))
+ ['foo']
+
+ If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+ returns ``True`` won't be considered iterable.
+
+ >>> obj = {'a': 1}
+ >>> list(always_iterable(obj)) # Iterate over the dict's keys
+ ['a']
+ >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
+ [{'a': 1}]
+
+ Set *base_type* to ``None`` to avoid any special handling and treat objects
+ Python considers iterable as iterable:
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj, base_type=None))
+ ['f', 'o', 'o']
+ """
+ if obj is None:
+ return iter(())
+
+ if (base_type is not None) and isinstance(obj, base_type):
+ return iter((obj,))
+
+ try:
+ return iter(obj)
+ except TypeError:
+ return iter((obj,))
diff --git a/lib/importlib_metadata/_meta.py b/lib/importlib_metadata/_meta.py
new file mode 100644
index 00000000..37ee43e6
--- /dev/null
+++ b/lib/importlib_metadata/_meta.py
@@ -0,0 +1,48 @@
+from ._compat import Protocol
+from typing import Any, Dict, Iterator, List, TypeVar, Union
+
+
+_T = TypeVar("_T")
+
+
+class PackageMetadata(Protocol):
+ def __len__(self) -> int:
+ ... # pragma: no cover
+
+ def __contains__(self, item: str) -> bool:
+ ... # pragma: no cover
+
+ def __getitem__(self, key: str) -> str:
+ ... # pragma: no cover
+
+ def __iter__(self) -> Iterator[str]:
+ ... # pragma: no cover
+
+ def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
+ """
+ Return all values associated with a possibly multi-valued key.
+ """
+
+ @property
+ def json(self) -> Dict[str, Union[str, List[str]]]:
+ """
+ A JSON-compatible form of the metadata.
+ """
+
+
+class SimplePath(Protocol):
+ """
+ A minimal subset of pathlib.Path required by PathDistribution.
+ """
+
+ def joinpath(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def __truediv__(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def parent(self) -> 'SimplePath':
+ ... # pragma: no cover
+
+ def read_text(self) -> str:
+ ... # pragma: no cover
diff --git a/lib/importlib_metadata/_text.py b/lib/importlib_metadata/_text.py
new file mode 100644
index 00000000..c88cfbb2
--- /dev/null
+++ b/lib/importlib_metadata/_text.py
@@ -0,0 +1,99 @@
+import re
+
+from ._functools import method_cache
+
+
+# from jaraco.text 3.5
+class FoldedCase(str):
+ """
+ A case insensitive string class; behaves just like str
+ except compares equal when the only variation is case.
+
+ >>> s = FoldedCase('hello world')
+
+ >>> s == 'Hello World'
+ True
+
+ >>> 'Hello World' == s
+ True
+
+ >>> s != 'Hello World'
+ False
+
+ >>> s.index('O')
+ 4
+
+ >>> s.split('O')
+ ['hell', ' w', 'rld']
+
+ >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
+ ['alpha', 'Beta', 'GAMMA']
+
+ Sequence membership is straightforward.
+
+ >>> "Hello World" in [s]
+ True
+ >>> s in ["Hello World"]
+ True
+
+ You may test for set inclusion, but candidate and elements
+ must both be folded.
+
+ >>> FoldedCase("Hello World") in {s}
+ True
+ >>> s in {FoldedCase("Hello World")}
+ True
+
+ String inclusion works as long as the FoldedCase object
+ is on the right.
+
+ >>> "hello" in FoldedCase("Hello World")
+ True
+
+ But not if the FoldedCase object is on the left:
+
+ >>> FoldedCase('hello') in 'Hello World'
+ False
+
+ In that case, use in_:
+
+ >>> FoldedCase('hello').in_('Hello World')
+ True
+
+ >>> FoldedCase('hello') > FoldedCase('Hello')
+ False
+ """
+
+ def __lt__(self, other):
+ return self.lower() < other.lower()
+
+ def __gt__(self, other):
+ return self.lower() > other.lower()
+
+ def __eq__(self, other):
+ return self.lower() == other.lower()
+
+ def __ne__(self, other):
+ return self.lower() != other.lower()
+
+ def __hash__(self):
+ return hash(self.lower())
+
+ def __contains__(self, other):
+ return super().lower().__contains__(other.lower())
+
+ def in_(self, other):
+ "Does self appear in other?"
+ return self in FoldedCase(other)
+
+ # cache lower since it's likely to be called frequently.
+ @method_cache
+ def lower(self):
+ return super().lower()
+
+ def index(self, sub):
+ return self.lower().index(sub.lower())
+
+ def split(self, splitter=' ', maxsplit=0):
+ pattern = re.compile(re.escape(splitter), re.I)
+ return pattern.split(self, maxsplit)
diff --git a/lib/importlib_metadata/py.typed b/lib/importlib_metadata/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/mako/__init__.py b/lib/mako/__init__.py
index df2ae480..5ae55011 100644
--- a/lib/mako/__init__.py
+++ b/lib/mako/__init__.py
@@ -1,8 +1,8 @@
# mako/__init__.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-__version__ = "1.1.6"
+__version__ = "1.2.0"
diff --git a/lib/mako/_ast_util.py b/lib/mako/_ast_util.py
index bdcdbf69..b8615335 100644
--- a/lib/mako/_ast_util.py
+++ b/lib/mako/_ast_util.py
@@ -1,5 +1,5 @@
# mako/_ast_util.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -47,7 +47,6 @@ from _ast import Sub
from _ast import UAdd
from _ast import USub
-from mako.compat import arg_stringname
BOOLOP_SYMBOLS = {And: "and", Or: "or"}
@@ -94,9 +93,7 @@ def parse(expr, filename="", mode="exec"):
def iter_fields(node):
"""Iterate over all fields of a node, only yielding existing fields."""
- # CPython 2.5 compat
- if not hasattr(node, "_fields") or not node._fields:
- return
+
for field in node._fields:
try:
yield field, getattr(node, field)
@@ -104,7 +101,7 @@ def iter_fields(node):
pass
-class NodeVisitor(object):
+class NodeVisitor:
"""
Walks the abstract syntax tree and call visitor functions for every node
@@ -266,10 +263,10 @@ class SourceGenerator(NodeVisitor):
self.visit(default)
if node.vararg is not None:
write_comma()
- self.write("*" + arg_stringname(node.vararg))
+ self.write("*" + node.vararg.arg)
if node.kwarg is not None:
write_comma()
- self.write("**" + arg_stringname(node.kwarg))
+ self.write("**" + node.kwarg.arg)
def decorators(self, node):
for decorator in node.decorator_list:
diff --git a/lib/mako/ast.py b/lib/mako/ast.py
index cfae2806..f879e8b4 100644
--- a/lib/mako/ast.py
+++ b/lib/mako/ast.py
@@ -1,5 +1,5 @@
# mako/ast.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -9,12 +9,11 @@ code, as well as generating Python from AST nodes"""
import re
-from mako import compat
from mako import exceptions
from mako import pyparser
-class PythonCode(object):
+class PythonCode:
"""represents information about a string containing Python code"""
@@ -39,7 +38,7 @@ class PythonCode(object):
# - AST is less likely to break with version changes
# (for example, the behavior of co_names changed a little bit
# in python version 2.5)
- if isinstance(code, compat.string_types):
+ if isinstance(code, str):
expr = pyparser.parse(code.lstrip(), "exec", **exception_kwargs)
else:
expr = code
@@ -48,7 +47,7 @@ class PythonCode(object):
f.visit(expr)
-class ArgumentList(object):
+class ArgumentList:
"""parses a fragment of code as a comma-separated list of expressions"""
@@ -57,7 +56,7 @@ class ArgumentList(object):
self.args = []
self.declared_identifiers = set()
self.undeclared_identifiers = set()
- if isinstance(code, compat.string_types):
+ if isinstance(code, str):
if re.match(r"\S", code) and not re.match(r",\s*$", code):
# if theres text and no trailing comma, insure its parsed
# as a tuple by adding a trailing comma
@@ -88,7 +87,7 @@ class PythonFragment(PythonCode):
if not m:
raise exceptions.CompileException(
"Fragment '%s' is not a partial control statement" % code,
- **exception_kwargs
+ **exception_kwargs,
)
if m.group(3):
code = code[: m.start(3)]
@@ -97,7 +96,7 @@ class PythonFragment(PythonCode):
code = code + "pass"
elif keyword == "try":
code = code + "pass\nexcept:pass"
- elif keyword == "elif" or keyword == "else":
+ elif keyword in ["elif", "else"]:
code = "if False:pass\n" + code + "pass"
elif keyword == "except":
code = "try:pass\n" + code + "pass"
@@ -106,12 +105,12 @@ class PythonFragment(PythonCode):
else:
raise exceptions.CompileException(
"Unsupported control keyword: '%s'" % keyword,
- **exception_kwargs
+ **exception_kwargs,
)
- super(PythonFragment, self).__init__(code, **exception_kwargs)
+ super().__init__(code, **exception_kwargs)
-class FunctionDecl(object):
+class FunctionDecl:
"""function declaration"""
@@ -124,13 +123,13 @@ class FunctionDecl(object):
if not hasattr(self, "funcname"):
raise exceptions.CompileException(
"Code '%s' is not a function declaration" % code,
- **exception_kwargs
+ **exception_kwargs,
)
if not allow_kwargs and self.kwargs:
raise exceptions.CompileException(
"'**%s' keyword argument not allowed here"
% self.kwargnames[-1],
- **exception_kwargs
+ **exception_kwargs,
)
def get_argument_expressions(self, as_call=False):
@@ -200,6 +199,4 @@ class FunctionArgs(FunctionDecl):
"""the argument portion of a function declaration"""
def __init__(self, code, **kwargs):
- super(FunctionArgs, self).__init__(
- "def ANON(%s):pass" % code, **kwargs
- )
+ super().__init__("def ANON(%s):pass" % code, **kwargs)
diff --git a/lib/mako/cache.py b/lib/mako/cache.py
index 26aa93ee..d77be271 100644
--- a/lib/mako/cache.py
+++ b/lib/mako/cache.py
@@ -1,10 +1,9 @@
# mako/cache.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from mako import compat
from mako import util
_cache_plugins = util.PluginLoader("mako.cache")
@@ -13,7 +12,7 @@ register_plugin = _cache_plugins.register
register_plugin("beaker", "mako.ext.beaker_cache", "BeakerCacheImpl")
-class Cache(object):
+class Cache:
"""Represents a data content cache made available to the module
space of a specific :class:`.Template` object.
@@ -66,7 +65,7 @@ class Cache(object):
def __init__(self, template, *args):
# check for a stale template calling the
# constructor
- if isinstance(template, compat.string_types) and args:
+ if isinstance(template, str) and args:
return
self.template = template
self.id = template.module.__name__
@@ -181,7 +180,7 @@ class Cache(object):
return tmpl_kw
-class CacheImpl(object):
+class CacheImpl:
"""Provide a cache implementation for use by :class:`.Cache`."""
diff --git a/lib/mako/cmd.py b/lib/mako/cmd.py
index c0f2c754..7592fb27 100755
--- a/lib/mako/cmd.py
+++ b/lib/mako/cmd.py
@@ -1,10 +1,9 @@
# mako/cmd.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
from argparse import ArgumentParser
-import io
from os.path import dirname
from os.path import isfile
import sys
@@ -85,16 +84,14 @@ def cmdline(argv=None):
except:
_exit()
- kw = dict([varsplit(var) for var in options.var])
+ kw = dict(varsplit(var) for var in options.var)
try:
rendered = template.render(**kw)
except:
_exit()
else:
if output_file:
- io.open(output_file, "wt", encoding=output_encoding).write(
- rendered
- )
+ open(output_file, "wt", encoding=output_encoding).write(rendered)
else:
sys.stdout.write(rendered)
diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py
index a9ae55b8..c897f0ff 100644
--- a/lib/mako/codegen.py
+++ b/lib/mako/codegen.py
@@ -1,5 +1,5 @@
# mako/codegen.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -12,7 +12,6 @@ import re
import time
from mako import ast
-from mako import compat
from mako import exceptions
from mako import filters
from mako import parsetree
@@ -25,8 +24,8 @@ MAGIC_NUMBER = 10
# names which are hardwired into the
# template and are not accessed via the
# context itself
-TOPLEVEL_DECLARED = set(["UNDEFINED", "STOP_RENDERING"])
-RESERVED_NAMES = set(["context", "loop"]).union(TOPLEVEL_DECLARED)
+TOPLEVEL_DECLARED = {"UNDEFINED", "STOP_RENDERING"}
+RESERVED_NAMES = {"context", "loop"}.union(TOPLEVEL_DECLARED)
def compile( # noqa
@@ -39,20 +38,12 @@ def compile( # noqa
future_imports=None,
source_encoding=None,
generate_magic_comment=True,
- disable_unicode=False,
strict_undefined=False,
enable_loop=True,
reserved_names=frozenset(),
):
"""Generate module source code given a parsetree node,
- uri, and optional source filename"""
-
- # if on Py2K, push the "source_encoding" string to be
- # a bytestring itself, as we will be embedding it into
- # the generated source and we don't want to coerce the
- # result into a unicode object, in "disable_unicode" mode
- if not compat.py3k and isinstance(source_encoding, compat.text_type):
- source_encoding = source_encoding.encode(source_encoding)
+ uri, and optional source filename"""
buf = util.FastEncodingBuffer()
@@ -68,7 +59,6 @@ def compile( # noqa
future_imports,
source_encoding,
generate_magic_comment,
- disable_unicode,
strict_undefined,
enable_loop,
reserved_names,
@@ -78,7 +68,7 @@ def compile( # noqa
return buf.getvalue()
-class _CompileContext(object):
+class _CompileContext:
def __init__(
self,
uri,
@@ -89,7 +79,6 @@ class _CompileContext(object):
future_imports,
source_encoding,
generate_magic_comment,
- disable_unicode,
strict_undefined,
enable_loop,
reserved_names,
@@ -102,16 +91,15 @@ class _CompileContext(object):
self.future_imports = future_imports
self.source_encoding = source_encoding
self.generate_magic_comment = generate_magic_comment
- self.disable_unicode = disable_unicode
self.strict_undefined = strict_undefined
self.enable_loop = enable_loop
self.reserved_names = reserved_names
-class _GenerateRenderMethod(object):
+class _GenerateRenderMethod:
"""A template visitor object which generates the
- full module source for a template.
+ full module source for a template.
"""
@@ -196,7 +184,7 @@ class _GenerateRenderMethod(object):
self.compiler.pagetag = None
- class FindTopLevel(object):
+ class FindTopLevel:
def visitInheritTag(s, node):
inherit.append(node)
@@ -392,7 +380,7 @@ class _GenerateRenderMethod(object):
identifiers = self.compiler.identifiers.branch(node)
self.in_def = True
- class NSDefVisitor(object):
+ class NSDefVisitor:
def visitDefTag(s, node):
s.visitDefOrBase(node)
@@ -404,7 +392,7 @@ class _GenerateRenderMethod(object):
raise exceptions.CompileException(
"Can't put anonymous blocks inside "
"<%namespace>",
- **node.exception_kwargs
+ **node.exception_kwargs,
)
self.write_inline_def(node, identifiers, nested=False)
export.append(node.funcname)
@@ -481,7 +469,7 @@ class _GenerateRenderMethod(object):
"""
# collection of all defs available to us in this scope
- comp_idents = dict([(c.funcname, c) for c in identifiers.defs])
+ comp_idents = {c.funcname: c for c in identifiers.defs}
to_write = set()
# write "context.get()" for all variables we are going to
@@ -714,7 +702,7 @@ class _GenerateRenderMethod(object):
toplevel=False,
):
"""write a post-function decorator to replace a rendering
- callable with a cached version of itself."""
+ callable with a cached version of itself."""
self.printer.writeline("__M_%s = %s" % (name, name))
cachekey = node_or_pagetag.parsed_attributes.get(
@@ -794,8 +782,6 @@ class _GenerateRenderMethod(object):
def locate_encode(name):
if re.match(r"decode\..+", name):
return "filters." + name
- elif self.compiler.disable_unicode:
- return filters.NON_UNICODE_ESCAPES.get(name, name)
else:
return filters.DEFAULT_ESCAPES.get(name, name)
@@ -859,11 +845,11 @@ class _GenerateRenderMethod(object):
# and end control lines, and
# 3) any control line with no content other than comments
if not children or (
- compat.all(
+ all(
isinstance(c, (parsetree.Comment, parsetree.ControlLine))
for c in children
)
- and compat.all(
+ and all(
(node.is_ternary(c.keyword) or c.isend)
for c in children
if isinstance(c, parsetree.ControlLine)
@@ -969,7 +955,7 @@ class _GenerateRenderMethod(object):
self.identifier_stack.append(body_identifiers)
- class DefVisitor(object):
+ class DefVisitor:
def visitDefTag(s, node):
s.visitDefOrBase(node)
@@ -1025,7 +1011,7 @@ class _GenerateRenderMethod(object):
)
-class _Identifiers(object):
+class _Identifiers:
"""tracks the status of identifier names as template code is rendered."""
@@ -1098,7 +1084,7 @@ class _Identifiers(object):
def branch(self, node, **kwargs):
"""create a new Identifiers for a new Node, with
- this Identifiers as the parent."""
+ this Identifiers as the parent."""
return _Identifiers(self.compiler, node, self, **kwargs)
@@ -1123,7 +1109,7 @@ class _Identifiers(object):
def check_declared(self, node):
"""update the state of this Identifiers with the undeclared
- and declared identifiers of the given node."""
+ and declared identifiers of the given node."""
for ident in node.undeclared_identifiers():
if ident != "context" and ident not in self.declared.union(
@@ -1170,7 +1156,7 @@ class _Identifiers(object):
raise exceptions.CompileException(
"%%def or %%block named '%s' already "
"exists in this template." % node.funcname,
- **node.exception_kwargs
+ **node.exception_kwargs,
)
def visitDefTag(self, node):
@@ -1200,7 +1186,7 @@ class _Identifiers(object):
raise exceptions.CompileException(
"Named block '%s' not allowed inside of def '%s'"
% (node.name, self.node.name),
- **node.exception_kwargs
+ **node.exception_kwargs,
)
elif isinstance(
self.node, (parsetree.CallTag, parsetree.CallNamespaceTag)
@@ -1208,7 +1194,7 @@ class _Identifiers(object):
raise exceptions.CompileException(
"Named block '%s' not allowed inside of <%%call> tag"
% (node.name,),
- **node.exception_kwargs
+ **node.exception_kwargs,
)
for ident in node.undeclared_identifiers():
@@ -1293,7 +1279,7 @@ def mangle_mako_loop(node, printer):
return text
-class LoopVariable(object):
+class LoopVariable:
"""A node visitor which looks for the name 'loop' within undeclared
identifiers."""
diff --git a/lib/mako/compat.py b/lib/mako/compat.py
index 06bb8d99..68bc03b1 100644
--- a/lib/mako/compat.py
+++ b/lib/mako/compat.py
@@ -1,19 +1,17 @@
# mako/compat.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
import collections
+from importlib import util
import inspect
import sys
-py3k = sys.version_info >= (3, 0)
-py2k = sys.version_info < (3,)
-py27 = sys.version_info >= (2, 7)
-jython = sys.platform.startswith("java")
win32 = sys.platform.startswith("win")
pypy = hasattr(sys, "pypy_version_info")
+py38 = sys.version_info >= (3, 8)
ArgSpec = collections.namedtuple(
"ArgSpec", ["args", "varargs", "keywords", "defaults"]
@@ -26,15 +24,15 @@ def inspect_getargspec(func):
if inspect.ismethod(func):
func = func.__func__
if not inspect.isfunction(func):
- raise TypeError("{!r} is not a Python function".format(func))
+ raise TypeError(f"{func!r} is not a Python function")
co = func.__code__
if not inspect.iscode(co):
- raise TypeError("{!r} is not a code object".format(co))
+ raise TypeError(f"{co!r} is not a code object")
nargs = co.co_argcount
names = co.co_varnames
- nkwargs = co.co_kwonlyargcount if py3k else 0
+ nkwargs = co.co_kwonlyargcount
args = list(names[:nargs])
nargs += nkwargs
@@ -49,129 +47,30 @@ def inspect_getargspec(func):
return ArgSpec(args, varargs, varkw, func.__defaults__)
-if py3k:
- from io import StringIO
- import builtins as compat_builtins
- from urllib.parse import quote_plus, unquote_plus
- from html.entities import codepoint2name, name2codepoint
-
- string_types = (str,)
- binary_type = bytes
- text_type = str
-
- from io import BytesIO as byte_buffer
-
- def u(s):
- return s
-
- def b(s):
- return s.encode("latin-1")
-
- def octal(lit):
- return eval("0o" + lit)
-
-
-else:
- import __builtin__ as compat_builtins # noqa
-
- try:
- from cStringIO import StringIO
- except:
- from StringIO import StringIO
-
- byte_buffer = StringIO
-
- from urllib import quote_plus, unquote_plus # noqa
- from htmlentitydefs import codepoint2name, name2codepoint # noqa
-
- string_types = (basestring,) # noqa
- binary_type = str
- text_type = unicode # noqa
-
- def u(s):
- return unicode(s, "utf-8") # noqa
-
- def b(s):
- return s
-
- def octal(lit):
- return eval("0" + lit)
-
-
-if py3k:
- from importlib import machinery, util
-
- if hasattr(util, 'module_from_spec'):
- # Python 3.5+
- def load_module(module_id, path):
- spec = util.spec_from_file_location(module_id, path)
- module = util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
- else:
- def load_module(module_id, path):
- module = machinery.SourceFileLoader(module_id, path).load_module()
- del sys.modules[module_id]
- return module
-
-else:
- import imp
-
- def load_module(module_id, path):
- fp = open(path, "rb")
- try:
- module = imp.load_source(module_id, path, fp)
- del sys.modules[module_id]
- return module
- finally:
- fp.close()
-
-
-if py3k:
-
- def reraise(tp, value, tb=None, cause=None):
- if cause is not None:
- value.__cause__ = cause
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
-
-else:
- exec(
- "def reraise(tp, value, tb=None, cause=None):\n"
- " raise tp, value, tb\n"
- )
+def load_module(module_id, path):
+ spec = util.spec_from_file_location(module_id, path)
+ module = util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
def exception_as():
return sys.exc_info()[1]
-all = all # noqa
-
-
def exception_name(exc):
return exc.__class__.__name__
-################################################
-# cross-compatible metaclass implementation
-# Copyright (c) 2010-2012 Benjamin Peterson
-def with_metaclass(meta, base=object):
- """Create a base class with a metaclass."""
- return meta("%sBase" % meta.__name__, (base,), {})
+if py38:
+ from importlib import metadata as importlib_metadata
+else:
+ import importlib_metadata # noqa
-################################################
-
-
-def arg_stringname(func_arg):
- """Gets the string name of a kwarg or vararg
- In Python3.4 a function's args are
- of _ast.arg type not _ast.name
- """
- if hasattr(func_arg, "arg"):
- return func_arg.arg
+def importlib_metadata_get(group):
+ ep = importlib_metadata.entry_points()
+ if hasattr(ep, "select"):
+ return ep.select(group=group)
else:
- return str(func_arg)
+ return ep.get(group, ())
diff --git a/lib/mako/exceptions.py b/lib/mako/exceptions.py
index ea7b20db..a0a5feca 100644
--- a/lib/mako/exceptions.py
+++ b/lib/mako/exceptions.py
@@ -1,5 +1,5 @@
# mako/exceptions.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -68,7 +68,7 @@ class TopLevelLookupException(TemplateLookupException):
pass
-class RichTraceback(object):
+class RichTraceback:
"""Pull the current exception from the ``sys`` traceback and extracts
Mako-specific template information.
@@ -106,7 +106,7 @@ class RichTraceback(object):
def _init_message(self):
"""Find a unicode representation of self.error"""
try:
- self.message = compat.text_type(self.error)
+ self.message = str(self.error)
except UnicodeError:
try:
self.message = str(self.error)
@@ -114,8 +114,8 @@ class RichTraceback(object):
# Fallback to args as neither unicode nor
# str(Exception(u'\xe6')) work in Python < 2.6
self.message = self.error.args[0]
- if not isinstance(self.message, compat.text_type):
- self.message = compat.text_type(self.message, "ascii", "replace")
+ if not isinstance(self.message, str):
+ self.message = str(self.message, "ascii", "replace")
def _get_reformatted_records(self, records):
for rec in records:
@@ -139,8 +139,7 @@ class RichTraceback(object):
@property
def reverse_traceback(self):
- """Return the same data as traceback, except in reverse order.
- """
+ """Return the same data as traceback, except in reverse order."""
return list(self._get_reformatted_records(self.reverse_records))
@@ -170,17 +169,6 @@ class RichTraceback(object):
)
except KeyError:
# A normal .py file (not a Template)
- if not compat.py3k:
- try:
- fp = open(filename, "rb")
- encoding = util.parse_encoding(fp)
- fp.close()
- except IOError:
- encoding = None
- if encoding:
- line = line.decode(encoding)
- else:
- line = line.decode("ascii", "replace")
new_trcback.append(
(
filename,
@@ -236,13 +224,12 @@ class RichTraceback(object):
if new_trcback:
try:
# A normal .py file (not a Template)
- fp = open(new_trcback[-1][0], "rb")
- encoding = util.parse_encoding(fp)
- if compat.py3k and not encoding:
- encoding = "utf-8"
- fp.seek(0)
- self.source = fp.read()
- fp.close()
+ with open(new_trcback[-1][0], "rb") as fp:
+ encoding = util.parse_encoding(fp)
+ if not encoding:
+ encoding = "utf-8"
+ fp.seek(0)
+ self.source = fp.read()
if encoding:
self.source = self.source.decode(encoding)
except IOError:
diff --git a/lib/mako/ext/autohandler.py b/lib/mako/ext/autohandler.py
index 8b1324ef..e8fdac89 100644
--- a/lib/mako/ext/autohandler.py
+++ b/lib/mako/ext/autohandler.py
@@ -1,5 +1,5 @@
# ext/autohandler.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
diff --git a/lib/mako/ext/babelplugin.py b/lib/mako/ext/babelplugin.py
index 76bbc5b0..f015ec25 100644
--- a/lib/mako/ext/babelplugin.py
+++ b/lib/mako/ext/babelplugin.py
@@ -1,10 +1,10 @@
# ext/babelplugin.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""gettext message extraction via Babel: http://babel.edgewall.org/"""
+"""gettext message extraction via Babel: https://pypi.org/project/Babel/"""
from babel.messages.extract import extract_python
from mako.ext.extract import MessageExtractor
@@ -15,12 +15,12 @@ class BabelMakoExtractor(MessageExtractor):
self.keywords = keywords
self.options = options
self.config = {
- "comment-tags": u" ".join(comment_tags),
+ "comment-tags": " ".join(comment_tags),
"encoding": options.get(
"input_encoding", options.get("encoding", None)
),
}
- super(BabelMakoExtractor, self).__init__()
+ super().__init__()
def __call__(self, fileobj):
return self.process_file(fileobj)
@@ -54,5 +54,4 @@ def extract(fileobj, keywords, comment_tags, options):
:rtype: ``iterator``
"""
extractor = BabelMakoExtractor(keywords, comment_tags, options)
- for message in extractor(fileobj):
- yield message
+ yield from extractor(fileobj)
diff --git a/lib/mako/ext/beaker_cache.py b/lib/mako/ext/beaker_cache.py
index f65ce43a..a40b09cf 100644
--- a/lib/mako/ext/beaker_cache.py
+++ b/lib/mako/ext/beaker_cache.py
@@ -1,5 +1,5 @@
# ext/beaker_cache.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -40,7 +40,7 @@ class BeakerCacheImpl(CacheImpl):
_beaker_cache = cache.template.cache_args["manager"]
else:
_beaker_cache = beaker_cache.CacheManager()
- super(BeakerCacheImpl, self).__init__(cache)
+ super().__init__(cache)
def _get_cache(self, **kw):
expiretime = kw.pop("timeout", None)
diff --git a/lib/mako/ext/extract.py b/lib/mako/ext/extract.py
index ad2348a5..74d067d8 100644
--- a/lib/mako/ext/extract.py
+++ b/lib/mako/ext/extract.py
@@ -1,23 +1,25 @@
# ext/extract.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
+from io import BytesIO
+from io import StringIO
import re
-from mako import compat
from mako import lexer
from mako import parsetree
-class MessageExtractor(object):
+class MessageExtractor:
+ use_bytes = True
+
def process_file(self, fileobj):
template_node = lexer.Lexer(
fileobj.read(), input_encoding=self.config["encoding"]
).parse()
- for extracted in self.extract_nodes(template_node.get_children()):
- yield extracted
+ yield from self.extract_nodes(template_node.get_children())
def extract_nodes(self, nodes):
translator_comments = []
@@ -90,7 +92,7 @@ class MessageExtractor(object):
comment[1] for comment in translator_comments
]
- if isinstance(code, compat.text_type):
+ if isinstance(code, str) and self.use_bytes:
code = code.encode(input_encoding, "backslashreplace")
used_translator_comments = False
@@ -99,7 +101,10 @@ class MessageExtractor(object):
# input string of the input is non-ascii)
# Also, because we added it, we have to subtract one from
# node.lineno
- code = compat.byte_buffer(compat.b("\n") + code)
+ if self.use_bytes:
+ code = BytesIO(b"\n" + code)
+ else:
+ code = StringIO("\n" + code)
for message in self.process_python(
code, node.lineno - 1, translator_strings
@@ -112,8 +117,7 @@ class MessageExtractor(object):
in_translator_comments = False
if child_nodes:
- for extracted in self.extract_nodes(child_nodes):
- yield extracted
+ yield from self.extract_nodes(child_nodes)
@staticmethod
def _split_comment(lineno, comment):
diff --git a/lib/mako/ext/linguaplugin.py b/lib/mako/ext/linguaplugin.py
index c40fa748..4cce6262 100644
--- a/lib/mako/ext/linguaplugin.py
+++ b/lib/mako/ext/linguaplugin.py
@@ -1,23 +1,23 @@
# ext/linguaplugin.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
+import contextlib
import io
from lingua.extractors import Extractor
from lingua.extractors import get_extractor
from lingua.extractors import Message
-from mako import compat
from mako.ext.extract import MessageExtractor
class LinguaMakoExtractor(Extractor, MessageExtractor):
-
"""Mako templates"""
+ use_bytes = False
extensions = [".mako"]
default_config = {"encoding": "utf-8", "comment-tags": ""}
@@ -26,29 +26,21 @@ class LinguaMakoExtractor(Extractor, MessageExtractor):
self.filename = filename
self.python_extractor = get_extractor("x.py")
if fileobj is None:
- fileobj = open(filename, "rb")
- must_close = True
+ ctx = open(filename, "r")
else:
- must_close = False
- try:
- for message in self.process_file(fileobj):
- yield message
- finally:
- if must_close:
- fileobj.close()
+ ctx = contextlib.nullcontext(fileobj)
+ with ctx as file_:
+ yield from self.process_file(file_)
def process_python(self, code, code_lineno, translator_strings):
source = code.getvalue().strip()
- if source.endswith(compat.b(":")):
- if source in (
- compat.b("try:"),
- compat.b("else:"),
- ) or source.startswith(compat.b("except")):
- source = compat.b("") # Ignore try/except and else
- elif source.startswith(compat.b("elif")):
+ if source.endswith(":"):
+ if source in ("try:", "else:") or source.startswith("except"):
+ source = "" # Ignore try/except and else
+ elif source.startswith("elif"):
source = source[2:] # Replace "elif" with "if"
- source += compat.b("pass")
- code = io.BytesIO(source)
+ source += "pass"
+ code = io.StringIO(source)
for msg in self.python_extractor(
self.filename, self.options, code, code_lineno - 1
):
@@ -58,7 +50,7 @@ class LinguaMakoExtractor(Extractor, MessageExtractor):
msg.msgid,
msg.msgid_plural,
msg.flags,
- compat.u(" ").join(translator_strings + [msg.comment]),
+ " ".join(translator_strings + [msg.comment]),
msg.tcomment,
msg.location,
)
diff --git a/lib/mako/ext/preprocessors.py b/lib/mako/ext/preprocessors.py
index 9cc06214..6855eeb4 100644
--- a/lib/mako/ext/preprocessors.py
+++ b/lib/mako/ext/preprocessors.py
@@ -1,5 +1,5 @@
# ext/preprocessors.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
diff --git a/lib/mako/ext/pygmentplugin.py b/lib/mako/ext/pygmentplugin.py
index 943a67a4..38d6a71b 100644
--- a/lib/mako/ext/pygmentplugin.py
+++ b/lib/mako/ext/pygmentplugin.py
@@ -1,5 +1,5 @@
# ext/pygmentplugin.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -25,8 +25,6 @@ from pygments.token import Other
from pygments.token import String
from pygments.token import Text
-from mako import compat
-
class MakoLexer(RegexLexer):
name = "Mako"
@@ -108,7 +106,7 @@ class MakoHtmlLexer(DelegatingLexer):
aliases = ["html+mako"]
def __init__(self, **options):
- super(MakoHtmlLexer, self).__init__(HtmlLexer, MakoLexer, **options)
+ super().__init__(HtmlLexer, MakoLexer, **options)
class MakoXmlLexer(DelegatingLexer):
@@ -116,7 +114,7 @@ class MakoXmlLexer(DelegatingLexer):
aliases = ["xml+mako"]
def __init__(self, **options):
- super(MakoXmlLexer, self).__init__(XmlLexer, MakoLexer, **options)
+ super().__init__(XmlLexer, MakoLexer, **options)
class MakoJavascriptLexer(DelegatingLexer):
@@ -124,9 +122,7 @@ class MakoJavascriptLexer(DelegatingLexer):
aliases = ["js+mako", "javascript+mako"]
def __init__(self, **options):
- super(MakoJavascriptLexer, self).__init__(
- JavascriptLexer, MakoLexer, **options
- )
+ super().__init__(JavascriptLexer, MakoLexer, **options)
class MakoCssLexer(DelegatingLexer):
@@ -134,7 +130,7 @@ class MakoCssLexer(DelegatingLexer):
aliases = ["css+mako"]
def __init__(self, **options):
- super(MakoCssLexer, self).__init__(CssLexer, MakoLexer, **options)
+ super().__init__(CssLexer, MakoLexer, **options)
pygments_html_formatter = HtmlFormatter(
@@ -144,10 +140,7 @@ pygments_html_formatter = HtmlFormatter(
def syntax_highlight(filename="", language=None):
mako_lexer = MakoLexer()
- if compat.py3k:
- python_lexer = Python3Lexer()
- else:
- python_lexer = PythonLexer()
+ python_lexer = Python3Lexer()
if filename.startswith("memory:") or language == "mako":
return lambda string: highlight(
string, mako_lexer, pygments_html_formatter
diff --git a/lib/mako/ext/turbogears.py b/lib/mako/ext/turbogears.py
index 722a6b4b..413d9f74 100644
--- a/lib/mako/ext/turbogears.py
+++ b/lib/mako/ext/turbogears.py
@@ -1,5 +1,5 @@
# ext/turbogears.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -9,7 +9,7 @@ from mako.lookup import TemplateLookup
from mako.template import Template
-class TGPlugin(object):
+class TGPlugin:
"""TurboGears compatible Template Plugin."""
@@ -51,7 +51,7 @@ class TGPlugin(object):
def render(
self, info, format="html", fragment=False, template=None # noqa
):
- if isinstance(template, compat.string_types):
+ if isinstance(template, str):
template = self.load_template(template)
# Load extra vars func if provided
diff --git a/lib/mako/filters.py b/lib/mako/filters.py
index 0ae33ff4..26edd8ee 100644
--- a/lib/mako/filters.py
+++ b/lib/mako/filters.py
@@ -1,18 +1,19 @@
# mako/filters.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
import codecs
+from html.entities import codepoint2name
+from html.entities import name2codepoint
import re
+from urllib.parse import quote_plus
-from mako import compat
-from mako.compat import codepoint2name
-from mako.compat import name2codepoint
-from mako.compat import quote_plus
-from mako.compat import unquote_plus
+import markupsafe
+
+html_escape = markupsafe.escape
xml_escapes = {
"&": "&",
@@ -22,27 +23,6 @@ xml_escapes = {
"'": "'", # also ' in html-only
}
-# XXX: " is valid in HTML and XML
-# ' is not valid HTML, but is valid XML
-
-
-def legacy_html_escape(s):
- """legacy HTML escape for non-unicode mode."""
- s = s.replace("&", "&")
- s = s.replace(">", ">")
- s = s.replace("<", "<")
- s = s.replace('"', """)
- s = s.replace("'", "'")
- return s
-
-
-try:
- import markupsafe
-
- html_escape = markupsafe.escape
-except ImportError:
- html_escape = legacy_html_escape
-
def xml_escape(string):
return re.sub(r'([&<"\'>])', lambda m: xml_escapes[m.group()], string)
@@ -54,31 +34,19 @@ def url_escape(string):
return quote_plus(string)
-def legacy_url_escape(string):
- # convert into a list of octets
- return quote_plus(string)
-
-
-def url_unescape(string):
- text = unquote_plus(string)
- if not is_ascii_str(text):
- text = text.decode("utf8")
- return text
-
-
def trim(string):
return string.strip()
-class Decode(object):
+class Decode:
def __getattr__(self, key):
def decode(x):
- if isinstance(x, compat.text_type):
+ if isinstance(x, str):
return x
- elif not isinstance(x, compat.binary_type):
+ elif not isinstance(x, bytes):
return decode(str(x))
else:
- return compat.text_type(x, encoding=key)
+ return str(x, encoding=key)
return decode
@@ -86,24 +54,11 @@ class Decode(object):
decode = Decode()
-_ASCII_re = re.compile(r"\A[\x00-\x7f]*\Z")
-
-
-def is_ascii_str(text):
- return isinstance(text, str) and _ASCII_re.match(text)
-
-
-################################################################
-
-
-class XMLEntityEscaper(object):
+class XMLEntityEscaper:
def __init__(self, codepoint2name, name2codepoint):
- self.codepoint2entity = dict(
- [
- (c, compat.text_type("&%s;" % n))
- for c, n in codepoint2name.items()
- ]
- )
+ self.codepoint2entity = {
+ c: str("&%s;" % n) for c, n in codepoint2name.items()
+ }
self.name2codepoint = name2codepoint
def escape_entities(self, text):
@@ -111,7 +66,7 @@ class XMLEntityEscaper(object):
Only characters corresponding to a named entity are replaced.
"""
- return compat.text_type(text).translate(self.codepoint2entity)
+ return str(text).translate(self.codepoint2entity)
def __escape(self, m):
codepoint = ord(m.group())
@@ -131,9 +86,7 @@ class XMLEntityEscaper(object):
The return value is guaranteed to be ASCII.
"""
- return self.__escapable.sub(
- self.__escape, compat.text_type(text)
- ).encode("ascii")
+ return self.__escapable.sub(self.__escape, str(text)).encode("ascii")
# XXX: This regexp will not match all valid XML entity names__.
# (It punts on details involving involving CombiningChars and Extenders.)
@@ -183,37 +136,28 @@ def htmlentityreplace_errors(ex):
characters with HTML entities, or, if no HTML entity exists for
the character, XML character references::
- >>> u'The cost was \u20ac12.'.encode('latin1', 'htmlentityreplace')
+ >>> 'The cost was \u20ac12.'.encode('latin1', 'htmlentityreplace')
'The cost was €12.'
"""
if isinstance(ex, UnicodeEncodeError):
# Handle encoding errors
bad_text = ex.object[ex.start : ex.end]
text = _html_entities_escaper.escape(bad_text)
- return (compat.text_type(text), ex.end)
+ return (str(text), ex.end)
raise ex
codecs.register_error("htmlentityreplace", htmlentityreplace_errors)
-# TODO: options to make this dynamic per-compilation will be added in a later
-# release
DEFAULT_ESCAPES = {
"x": "filters.xml_escape",
"h": "filters.html_escape",
"u": "filters.url_escape",
"trim": "filters.trim",
"entity": "filters.html_entities_escape",
- "unicode": "unicode",
+ "unicode": "str",
"decode": "decode",
"str": "str",
"n": "n",
}
-
-if compat.py3k:
- DEFAULT_ESCAPES.update({"unicode": "str"})
-
-NON_UNICODE_ESCAPES = DEFAULT_ESCAPES.copy()
-NON_UNICODE_ESCAPES["h"] = "filters.legacy_html_escape"
-NON_UNICODE_ESCAPES["u"] = "filters.legacy_url_escape"
diff --git a/lib/mako/lexer.py b/lib/mako/lexer.py
index bbf0c3a5..527c4b51 100644
--- a/lib/mako/lexer.py
+++ b/lib/mako/lexer.py
@@ -1,5 +1,5 @@
# mako/lexer.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -9,7 +9,6 @@
import codecs
import re
-from mako import compat
from mako import exceptions
from mako import parsetree
from mako.pygen import adjust_whitespace
@@ -17,14 +16,9 @@ from mako.pygen import adjust_whitespace
_regexp_cache = {}
-class Lexer(object):
+class Lexer:
def __init__(
- self,
- text,
- filename=None,
- disable_unicode=False,
- input_encoding=None,
- preprocessor=None,
+ self, text, filename=None, input_encoding=None, preprocessor=None
):
self.text = text
self.filename = filename
@@ -36,14 +30,8 @@ class Lexer(object):
self.tag = []
self.control_line = []
self.ternary_stack = []
- self.disable_unicode = disable_unicode
self.encoding = input_encoding
- if compat.py3k and disable_unicode:
- raise exceptions.UnsupportedError(
- "Mako for Python 3 does not " "support disabling Unicode"
- )
-
if preprocessor is None:
self.preprocessor = []
elif not hasattr(preprocessor, "__iter__"):
@@ -66,10 +54,7 @@ class Lexer(object):
try:
reg = _regexp_cache[(regexp, flags)]
except KeyError:
- if flags:
- reg = re.compile(regexp, flags)
- else:
- reg = re.compile(regexp)
+ reg = re.compile(regexp, flags) if flags else re.compile(regexp)
_regexp_cache[(regexp, flags)] = reg
return self.match_reg(reg)
@@ -87,10 +72,7 @@ class Lexer(object):
match = reg.match(self.text, self.match_position)
if match:
(start, end) = match.span()
- if end == start:
- self.match_position = end + 1
- else:
- self.match_position = end
+ self.match_position = end + 1 if end == start else end
self.matched_lineno = self.lineno
lines = re.findall(r"\n", self.text[mp : self.match_position])
cp = mp - 1
@@ -98,10 +80,6 @@ class Lexer(object):
cp -= 1
self.matched_charpos = mp - cp
self.lineno += len(lines)
- # print "MATCHED:", match.group(0), "LINE START:",
- # self.matched_lineno, "LINE END:", self.lineno
- # print "MATCH:", regexp, "\n", self.text[mp : mp + 15], \
- # (match and "TRUE" or "FALSE")
return match
def parse_until_text(self, watch_nesting, *text):
@@ -161,12 +139,15 @@ class Lexer(object):
if self.control_line:
control_frame = self.control_line[-1]
control_frame.nodes.append(node)
- if not (
- isinstance(node, parsetree.ControlLine)
- and control_frame.is_ternary(node.keyword)
+ if (
+ not (
+ isinstance(node, parsetree.ControlLine)
+ and control_frame.is_ternary(node.keyword)
+ )
+ and self.ternary_stack
+ and self.ternary_stack[-1]
):
- if self.ternary_stack and self.ternary_stack[-1]:
- self.ternary_stack[-1][-1].nodes.append(node)
+ self.ternary_stack[-1][-1].nodes.append(node)
if isinstance(node, parsetree.Tag):
if len(self.tag):
node.parent = self.tag[-1]
@@ -188,18 +169,18 @@ class Lexer(object):
raise exceptions.SyntaxException(
"Keyword '%s' not a legal ternary for keyword '%s'"
% (node.keyword, self.control_line[-1].keyword),
- **self.exception_kwargs
+ **self.exception_kwargs,
)
_coding_re = re.compile(r"#.*coding[:=]\s*([-\w.]+).*\r?\n")
def decode_raw_stream(self, text, decode_raw, known_encoding, filename):
"""given string/unicode or bytes/string, determine encoding
- from magic encoding comment, return body as unicode
- or raw if decode_raw=False
+ from magic encoding comment, return body as unicode
+ or raw if decode_raw=False
"""
- if isinstance(text, compat.text_type):
+ if isinstance(text, str):
m = self._coding_re.match(text)
encoding = m and m.group(1) or known_encoding or "utf-8"
return encoding, text
@@ -219,11 +200,7 @@ class Lexer(object):
)
else:
m = self._coding_re.match(text.decode("utf-8", "ignore"))
- if m:
- parsed_encoding = m.group(1)
- else:
- parsed_encoding = known_encoding or "utf-8"
-
+ parsed_encoding = m.group(1) if m else known_encoding or "utf-8"
if decode_raw:
try:
text = text.decode(parsed_encoding)
@@ -241,7 +218,7 @@ class Lexer(object):
def parse(self):
self.encoding, self.text = self.decode_raw_stream(
- self.text, not self.disable_unicode, self.encoding, self.filename
+ self.text, True, self.encoding, self.filename
)
for preproc in self.preprocessor:
@@ -276,12 +253,13 @@ class Lexer(object):
if self.match_position > self.textlength:
break
- raise exceptions.CompileException("assertion failed")
+ # TODO: no coverage here
+ raise exceptions.MakoException("assertion failed")
if len(self.tag):
raise exceptions.SyntaxException(
"Unclosed tag: <%%%s>" % self.tag[-1].keyword,
- **self.exception_kwargs
+ **self.exception_kwargs,
)
if len(self.control_line):
raise exceptions.SyntaxException(
@@ -312,35 +290,34 @@ class Lexer(object):
re.I | re.S | re.X,
)
- if match:
- keyword, attr, isend = match.groups()
- self.keyword = keyword
- attributes = {}
- if attr:
- for att in re.findall(
- r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr
- ):
- key, val1, val2 = att
- text = val1 or val2
- text = text.replace("\r\n", "\n")
- attributes[key] = text
- self.append_node(parsetree.Tag, keyword, attributes)
- if isend:
- self.tag.pop()
- else:
- if keyword == "text":
- match = self.match(r"(.*?)(?=\%text>)", re.S)
- if not match:
- raise exceptions.SyntaxException(
- "Unclosed tag: <%%%s>" % self.tag[-1].keyword,
- **self.exception_kwargs
- )
- self.append_node(parsetree.Text, match.group(1))
- return self.match_tag_end()
- return True
- else:
+ if not match:
return False
+ keyword, attr, isend = match.groups()
+ self.keyword = keyword
+ attributes = {}
+ if attr:
+ for att in re.findall(
+ r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr
+ ):
+ key, val1, val2 = att
+ text = val1 or val2
+ text = text.replace("\r\n", "\n")
+ attributes[key] = text
+ self.append_node(parsetree.Tag, keyword, attributes)
+ if isend:
+ self.tag.pop()
+ elif keyword == "text":
+ match = self.match(r"(.*?)(?=\%text>)", re.S)
+ if not match:
+ raise exceptions.SyntaxException(
+ "Unclosed tag: <%%%s>" % self.tag[-1].keyword,
+ **self.exception_kwargs,
+ )
+ self.append_node(parsetree.Text, match.group(1))
+ return self.match_tag_end()
+ return True
+
def match_tag_end(self):
match = self.match(r"\%[\t ]*(.+?)[\t ]*>")
if match:
@@ -348,13 +325,13 @@ class Lexer(object):
raise exceptions.SyntaxException(
"Closing tag without opening tag: %%%s>"
% match.group(1),
- **self.exception_kwargs
+ **self.exception_kwargs,
)
elif self.tag[-1].keyword != match.group(1):
raise exceptions.SyntaxException(
"Closing tag %%%s> does not match tag: <%%%s>"
% (match.group(1), self.tag[-1].keyword),
- **self.exception_kwargs
+ **self.exception_kwargs,
)
self.tag.pop()
return True
@@ -363,15 +340,15 @@ class Lexer(object):
def match_end(self):
match = self.match(r"\Z", re.S)
- if match:
- string = match.group()
- if string:
- return string
- else:
- return True
- else:
+ if not match:
return False
+ string = match.group()
+ if string:
+ return string
+ else:
+ return True
+
def match_text(self):
match = self.match(
r"""
@@ -422,64 +399,63 @@ class Lexer(object):
def match_expression(self):
match = self.match(r"\${")
- if match:
- line, pos = self.matched_lineno, self.matched_charpos
- text, end = self.parse_until_text(True, r"\|", r"}")
- if end == "|":
- escapes, end = self.parse_until_text(True, r"}")
- else:
- escapes = ""
- text = text.replace("\r\n", "\n")
- self.append_node(
- parsetree.Expression,
- text,
- escapes.strip(),
- lineno=line,
- pos=pos,
- )
- return True
- else:
+ if not match:
return False
+ line, pos = self.matched_lineno, self.matched_charpos
+ text, end = self.parse_until_text(True, r"\|", r"}")
+ if end == "|":
+ escapes, end = self.parse_until_text(True, r"}")
+ else:
+ escapes = ""
+ text = text.replace("\r\n", "\n")
+ self.append_node(
+ parsetree.Expression,
+ text,
+ escapes.strip(),
+ lineno=line,
+ pos=pos,
+ )
+ return True
+
def match_control_line(self):
match = self.match(
r"(?<=^)[\t ]*(%(?!%)|##)[\t ]*((?:(?:\\\r?\n)|[^\r\n])*)"
r"(?:\r?\n|\Z)",
re.M,
)
- if match:
- operator = match.group(1)
- text = match.group(2)
- if operator == "%":
- m2 = re.match(r"(end)?(\w+)\s*(.*)", text)
- if not m2:
- raise exceptions.SyntaxException(
- "Invalid control line: '%s'" % text,
- **self.exception_kwargs
- )
- isend, keyword = m2.group(1, 2)
- isend = isend is not None
-
- if isend:
- if not len(self.control_line):
- raise exceptions.SyntaxException(
- "No starting keyword '%s' for '%s'"
- % (keyword, text),
- **self.exception_kwargs
- )
- elif self.control_line[-1].keyword != keyword:
- raise exceptions.SyntaxException(
- "Keyword '%s' doesn't match keyword '%s'"
- % (text, self.control_line[-1].keyword),
- **self.exception_kwargs
- )
- self.append_node(parsetree.ControlLine, keyword, isend, text)
- else:
- self.append_node(parsetree.Comment, text)
- return True
- else:
+ if not match:
return False
+ operator = match.group(1)
+ text = match.group(2)
+ if operator == "%":
+ m2 = re.match(r"(end)?(\w+)\s*(.*)", text)
+ if not m2:
+ raise exceptions.SyntaxException(
+ "Invalid control line: '%s'" % text,
+ **self.exception_kwargs,
+ )
+ isend, keyword = m2.group(1, 2)
+ isend = isend is not None
+
+ if isend:
+ if not len(self.control_line):
+ raise exceptions.SyntaxException(
+ "No starting keyword '%s' for '%s'" % (keyword, text),
+ **self.exception_kwargs,
+ )
+ elif self.control_line[-1].keyword != keyword:
+ raise exceptions.SyntaxException(
+ "Keyword '%s' doesn't match keyword '%s'"
+ % (text, self.control_line[-1].keyword),
+ **self.exception_kwargs,
+ )
+ self.append_node(parsetree.ControlLine, keyword, isend, text)
+ else:
+ self.append_node(parsetree.Comment, text)
+ return True
+
def match_comment(self):
"""matches the multiline version of a comment"""
match = self.match(r"<%doc>(.*?)%doc>", re.S)
diff --git a/lib/mako/lookup.py b/lib/mako/lookup.py
index 476326d4..7afe242b 100644
--- a/lib/mako/lookup.py
+++ b/lib/mako/lookup.py
@@ -1,5 +1,5 @@
# mako/lookup.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -8,18 +8,14 @@ import os
import posixpath
import re
import stat
+import threading
from mako import exceptions
from mako import util
from mako.template import Template
-try:
- import threading
-except:
- import dummy_threading as threading
-
-class TemplateCollection(object):
+class TemplateCollection:
"""Represent a collection of :class:`.Template` objects,
identifiable via URI.
@@ -34,7 +30,7 @@ class TemplateCollection(object):
:class:`.TemplateCollection` is an abstract class,
with the usual default implementation being :class:`.TemplateLookup`.
- """
+ """
def has_template(self, uri):
"""Return ``True`` if this :class:`.TemplateLookup` is
@@ -68,7 +64,7 @@ class TemplateCollection(object):
def filename_to_uri(self, uri, filename):
"""Convert the given ``filename`` to a URI relative to
- this :class:`.TemplateCollection`."""
+ this :class:`.TemplateCollection`."""
return uri
@@ -161,8 +157,6 @@ class TemplateLookup(TemplateCollection):
collection_size=-1,
format_exceptions=False,
error_handler=None,
- disable_unicode=False,
- bytestring_passthrough=False,
output_encoding=None,
encoding_errors="strict",
cache_args=None,
@@ -207,8 +201,6 @@ class TemplateLookup(TemplateCollection):
"format_exceptions": format_exceptions,
"error_handler": error_handler,
"include_error_handler": include_error_handler,
- "disable_unicode": disable_unicode,
- "bytestring_passthrough": bytestring_passthrough,
"output_encoding": output_encoding,
"cache_impl": cache_impl,
"encoding_errors": encoding_errors,
@@ -249,7 +241,7 @@ class TemplateLookup(TemplateCollection):
return self._check(uri, self._collection[uri])
else:
return self._collection[uri]
- except KeyError:
+ except KeyError as e:
u = re.sub(r"^\/+", "", uri)
for dir_ in self.directories:
# make sure the path seperators are posix - os.altsep is empty
@@ -260,8 +252,8 @@ class TemplateLookup(TemplateCollection):
return self._load(srcfile, uri)
else:
raise exceptions.TopLevelLookupException(
- "Cant locate template for uri %r" % uri
- )
+ "Can't locate template for uri %r" % uri
+ ) from e
def adjust_uri(self, uri, relativeto):
"""Adjust the given ``uri`` based on the given relative URI."""
@@ -270,20 +262,19 @@ class TemplateLookup(TemplateCollection):
if key in self._uri_cache:
return self._uri_cache[key]
- if uri[0] != "/":
- if relativeto is not None:
- v = self._uri_cache[key] = posixpath.join(
- posixpath.dirname(relativeto), uri
- )
- else:
- v = self._uri_cache[key] = "/" + uri
- else:
+ if uri[0] == "/":
v = self._uri_cache[key] = uri
+ elif relativeto is not None:
+ v = self._uri_cache[key] = posixpath.join(
+ posixpath.dirname(relativeto), uri
+ )
+ else:
+ v = self._uri_cache[key] = "/" + uri
return v
def filename_to_uri(self, filename):
"""Convert the given ``filename`` to a URI relative to
- this :class:`.TemplateCollection`."""
+ this :class:`.TemplateCollection`."""
try:
return self._uri_cache[filename]
@@ -294,7 +285,7 @@ class TemplateLookup(TemplateCollection):
def _relativeize(self, filename):
"""Return the portion of a filename that is 'relative'
- to the directories in this lookup.
+ to the directories in this lookup.
"""
@@ -324,7 +315,7 @@ class TemplateLookup(TemplateCollection):
filename=posixpath.normpath(filename),
lookup=self,
module_filename=module_filename,
- **self.template_args
+ **self.template_args,
)
return template
except:
@@ -342,16 +333,15 @@ class TemplateLookup(TemplateCollection):
try:
template_stat = os.stat(template.filename)
- if template.module._modified_time < template_stat[stat.ST_MTIME]:
- self._collection.pop(uri, None)
- return self._load(template.filename, uri)
- else:
+ if template.module._modified_time >= template_stat[stat.ST_MTIME]:
return template
- except OSError:
+ self._collection.pop(uri, None)
+ return self._load(template.filename, uri)
+ except OSError as e:
self._collection.pop(uri, None)
raise exceptions.TemplateLookupException(
- "Cant locate template for uri %r" % uri
- )
+ "Can't locate template for uri %r" % uri
+ ) from e
def put_string(self, uri, text):
"""Place a new :class:`.Template` object into this
diff --git a/lib/mako/parsetree.py b/lib/mako/parsetree.py
index 801e48a7..2135769f 100644
--- a/lib/mako/parsetree.py
+++ b/lib/mako/parsetree.py
@@ -1,5 +1,5 @@
# mako/parsetree.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -9,13 +9,12 @@
import re
from mako import ast
-from mako import compat
from mako import exceptions
from mako import filters
from mako import util
-class Node(object):
+class Node:
"""base class for a Node in the parse tree."""
@@ -51,7 +50,7 @@ class TemplateNode(Node):
"""a 'container' node that stores the overall collection of nodes."""
def __init__(self, filename):
- super(TemplateNode, self).__init__("", 0, 0, filename)
+ super().__init__("", 0, 0, filename)
self.nodes = []
self.page_attributes = {}
@@ -80,7 +79,7 @@ class ControlLine(Node):
has_loop_context = False
def __init__(self, keyword, isend, text, **kwargs):
- super(ControlLine, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.text = text
self.keyword = keyword
self.isend = isend
@@ -107,11 +106,13 @@ class ControlLine(Node):
"""return true if the given keyword is a ternary keyword
for this ControlLine"""
- return keyword in {
- "if": set(["else", "elif"]),
- "try": set(["except", "finally"]),
- "for": set(["else"]),
- }.get(self.keyword, [])
+ cases = {
+ "if": {"else", "elif"},
+ "try": {"except", "finally"},
+ "for": {"else"},
+ }
+
+ return keyword in cases.get(self.keyword, set())
def __repr__(self):
return "ControlLine(%r, %r, %r, %r)" % (
@@ -123,11 +124,10 @@ class ControlLine(Node):
class Text(Node):
-
"""defines plain text in the template."""
def __init__(self, content, **kwargs):
- super(Text, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.content = content
def __repr__(self):
@@ -135,7 +135,6 @@ class Text(Node):
class Code(Node):
-
"""defines a Python code block, either inline or module level.
e.g.::
@@ -153,7 +152,7 @@ class Code(Node):
"""
def __init__(self, text, ismodule, **kwargs):
- super(Code, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.text = text
self.ismodule = ismodule
self.code = ast.PythonCode(text, **self.exception_kwargs)
@@ -173,7 +172,6 @@ class Code(Node):
class Comment(Node):
-
"""defines a comment line.
# this is a comment
@@ -181,7 +179,7 @@ class Comment(Node):
"""
def __init__(self, text, **kwargs):
- super(Comment, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.text = text
def __repr__(self):
@@ -189,7 +187,6 @@ class Comment(Node):
class Expression(Node):
-
"""defines an inline expression.
${x+y}
@@ -197,7 +194,7 @@ class Expression(Node):
"""
def __init__(self, text, escapes, **kwargs):
- super(Expression, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.text = text
self.escapes = escapes
self.escapes_code = ast.ArgumentList(escapes, **self.exception_kwargs)
@@ -210,7 +207,7 @@ class Expression(Node):
# TODO: make the "filter" shortcut list configurable at parse/gen time
return self.code.undeclared_identifiers.union(
self.escapes_code.undeclared_identifiers.difference(
- set(filters.DEFAULT_ESCAPES.keys())
+ filters.DEFAULT_ESCAPES
)
).difference(self.code.declared_identifiers)
@@ -223,7 +220,6 @@ class Expression(Node):
class _TagMeta(type):
-
"""metaclass to allow Tag to produce a subclass according to
its keyword"""
@@ -232,7 +228,7 @@ class _TagMeta(type):
def __init__(cls, clsname, bases, dict_):
if getattr(cls, "__keyword__", None) is not None:
cls._classmap[cls.__keyword__] = cls
- super(_TagMeta, cls).__init__(clsname, bases, dict_)
+ super().__init__(clsname, bases, dict_)
def __call__(cls, keyword, attributes, **kwargs):
if ":" in keyword:
@@ -254,7 +250,7 @@ class _TagMeta(type):
return type.__call__(cls, keyword, attributes, **kwargs)
-class Tag(compat.with_metaclass(_TagMeta, Node)):
+class Tag(Node, metaclass=_TagMeta):
"""abstract base class for tags.
e.g.::
@@ -276,7 +272,7 @@ class Tag(compat.with_metaclass(_TagMeta, Node)):
expressions,
nonexpressions,
required,
- **kwargs
+ **kwargs,
):
r"""construct a new Tag instance.
@@ -297,17 +293,20 @@ class Tag(compat.with_metaclass(_TagMeta, Node)):
other arguments passed to the Node superclass (lineno, pos)
"""
- super(Tag, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.keyword = keyword
self.attributes = attributes
self._parse_attributes(expressions, nonexpressions)
missing = [r for r in required if r not in self.parsed_attributes]
if len(missing):
raise exceptions.CompileException(
- "Missing attribute(s): %s"
- % ",".join([repr(m) for m in missing]),
- **self.exception_kwargs
+ (
+ "Missing attribute(s): %s"
+ % ",".join(repr(m) for m in missing)
+ ),
+ **self.exception_kwargs,
)
+
self.parent = None
self.nodes = []
@@ -339,23 +338,22 @@ class Tag(compat.with_metaclass(_TagMeta, Node)):
code.undeclared_identifiers
)
expr.append("(%s)" % m.group(1))
- else:
- if x:
- expr.append(repr(x))
+ elif x:
+ expr.append(repr(x))
self.parsed_attributes[key] = " + ".join(expr) or repr("")
elif key in nonexpressions:
if re.search(r"\${.+?}", self.attributes[key]):
raise exceptions.CompileException(
- "Attibute '%s' in tag '%s' does not allow embedded "
+ "Attribute '%s' in tag '%s' does not allow embedded "
"expressions" % (key, self.keyword),
- **self.exception_kwargs
+ **self.exception_kwargs,
)
self.parsed_attributes[key] = repr(self.attributes[key])
else:
raise exceptions.CompileException(
"Invalid attribute for tag '%s': '%s'"
% (self.keyword, key),
- **self.exception_kwargs
+ **self.exception_kwargs,
)
self.expression_undeclared_identifiers = undeclared_identifiers
@@ -379,13 +377,13 @@ class IncludeTag(Tag):
__keyword__ = "include"
def __init__(self, keyword, attributes, **kwargs):
- super(IncludeTag, self).__init__(
+ super().__init__(
keyword,
attributes,
("file", "import", "args"),
(),
("file",),
- **kwargs
+ **kwargs,
)
self.page_args = ast.PythonCode(
"__DUMMY(%s)" % attributes.get("args", ""), **self.exception_kwargs
@@ -396,24 +394,22 @@ class IncludeTag(Tag):
def undeclared_identifiers(self):
identifiers = self.page_args.undeclared_identifiers.difference(
- set(["__DUMMY"])
+ {"__DUMMY"}
).difference(self.page_args.declared_identifiers)
- return identifiers.union(
- super(IncludeTag, self).undeclared_identifiers()
- )
+ return identifiers.union(super().undeclared_identifiers())
class NamespaceTag(Tag):
__keyword__ = "namespace"
def __init__(self, keyword, attributes, **kwargs):
- super(NamespaceTag, self).__init__(
+ super().__init__(
keyword,
attributes,
("file",),
("name", "inheritable", "import", "module"),
(),
- **kwargs
+ **kwargs,
)
self.name = attributes.get("name", "__anon_%s" % hex(abs(id(self))))
@@ -421,12 +417,12 @@ class NamespaceTag(Tag):
raise exceptions.CompileException(
"'name' and/or 'import' attributes are required "
"for <%namespace>",
- **self.exception_kwargs
+ **self.exception_kwargs,
)
if "file" in attributes and "module" in attributes:
raise exceptions.CompileException(
"<%namespace> may only have one of 'file' or 'module'",
- **self.exception_kwargs
+ **self.exception_kwargs,
)
def declared_identifiers(self):
@@ -437,9 +433,7 @@ class TextTag(Tag):
__keyword__ = "text"
def __init__(self, keyword, attributes, **kwargs):
- super(TextTag, self).__init__(
- keyword, attributes, (), ("filter"), (), **kwargs
- )
+ super().__init__(keyword, attributes, (), ("filter"), (), **kwargs)
self.filter_args = ast.ArgumentList(
attributes.get("filter", ""), **self.exception_kwargs
)
@@ -458,13 +452,13 @@ class DefTag(Tag):
c for c in attributes if c.startswith("cache_")
]
- super(DefTag, self).__init__(
+ super().__init__(
keyword,
attributes,
expressions,
("name", "filter", "decorator"),
("name",),
- **kwargs
+ **kwargs,
)
name = attributes["name"]
if re.match(r"^[\w_]+$", name):
@@ -521,19 +515,19 @@ class BlockTag(Tag):
c for c in attributes if c.startswith("cache_")
]
- super(BlockTag, self).__init__(
+ super().__init__(
keyword,
attributes,
expressions,
("name", "filter", "decorator"),
(),
- **kwargs
+ **kwargs,
)
name = attributes.get("name")
if name and not re.match(r"^[\w_]+$", name):
raise exceptions.CompileException(
"%block may not specify an argument signature",
- **self.exception_kwargs
+ **self.exception_kwargs,
)
if not name and attributes.get("args", None):
raise exceptions.CompileException(
@@ -577,7 +571,7 @@ class CallTag(Tag):
__keyword__ = "call"
def __init__(self, keyword, attributes, **kwargs):
- super(CallTag, self).__init__(
+ super().__init__(
keyword, attributes, ("args"), ("expr",), ("expr",), **kwargs
)
self.expression = attributes["expr"]
@@ -597,26 +591,25 @@ class CallTag(Tag):
class CallNamespaceTag(Tag):
def __init__(self, namespace, defname, attributes, **kwargs):
- super(CallNamespaceTag, self).__init__(
+ super().__init__(
namespace + ":" + defname,
attributes,
tuple(attributes.keys()) + ("args",),
(),
(),
- **kwargs
+ **kwargs,
)
self.expression = "%s.%s(%s)" % (
namespace,
defname,
",".join(
- [
- "%s=%s" % (k, v)
- for k, v in self.parsed_attributes.items()
- if k != "args"
- ]
+ "%s=%s" % (k, v)
+ for k, v in self.parsed_attributes.items()
+ if k != "args"
),
)
+
self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
self.body_decl = ast.FunctionArgs(
attributes.get("args", ""), **self.exception_kwargs
@@ -635,7 +628,7 @@ class InheritTag(Tag):
__keyword__ = "inherit"
def __init__(self, keyword, attributes, **kwargs):
- super(InheritTag, self).__init__(
+ super().__init__(
keyword, attributes, ("file",), (), ("file",), **kwargs
)
@@ -651,9 +644,7 @@ class PageTag(Tag):
"enable_loop",
] + [c for c in attributes if c.startswith("cache_")]
- super(PageTag, self).__init__(
- keyword, attributes, expressions, (), (), **kwargs
- )
+ super().__init__(keyword, attributes, expressions, (), (), **kwargs)
self.body_decl = ast.FunctionArgs(
attributes.get("args", ""), **self.exception_kwargs
)
diff --git a/lib/mako/pygen.py b/lib/mako/pygen.py
index 947721f1..46b0b52f 100644
--- a/lib/mako/pygen.py
+++ b/lib/mako/pygen.py
@@ -1,5 +1,5 @@
# mako/pygen.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -11,7 +11,7 @@ import re
from mako import exceptions
-class PythonPrinter(object):
+class PythonPrinter:
def __init__(self, stream):
# indentation counter
self.indent = 0
@@ -96,18 +96,19 @@ class PythonPrinter(object):
is_comment = line and len(line) and line[0] == "#"
# see if this line should decrease the indentation level
- if not is_comment and (not hastext or self._is_unindentor(line)):
-
- if self.indent > 0:
- self.indent -= 1
- # if the indent_detail stack is empty, the user
- # probably put extra closures - the resulting
- # module wont compile.
- if len(self.indent_detail) == 0:
- raise exceptions.SyntaxException(
- "Too many whitespace closures"
- )
- self.indent_detail.pop()
+ if (
+ not is_comment
+ and (not hastext or self._is_unindentor(line))
+ and self.indent > 0
+ ):
+ self.indent -= 1
+ # if the indent_detail stack is empty, the user
+ # probably put extra closures - the resulting
+ # module wont compile.
+ if len(self.indent_detail) == 0:
+ # TODO: no coverage here
+ raise exceptions.MakoException("Too many whitespace closures")
+ self.indent_detail.pop()
if line is None:
return
@@ -167,13 +168,10 @@ class PythonPrinter(object):
# if the current line doesnt have one of the "unindentor" keywords,
# return False
match = re.match(r"^\s*(else|elif|except|finally).*\:", line)
- if not match:
- return False
-
- # whitespace matches up, we have a compound indentor,
+ # if True, whitespace matches up, we have a compound indentor,
# and this line has an unindentor, this
# is probably good enough
- return True
+ return bool(match)
# should we decide that its not good enough, heres
# more stuff to check.
@@ -218,11 +216,7 @@ class PythonPrinter(object):
current_state = self.backslashed or self.triplequoted
- if re.search(r"\\$", line):
- self.backslashed = True
- else:
- self.backslashed = False
-
+ self.backslashed = bool(re.search(r"\\$", line))
triples = len(re.findall(r"\"\"\"|\'\'\'", line))
if triples == 1 or triples % 2 != 0:
self.triplequoted = not self.triplequoted
diff --git a/lib/mako/pyparser.py b/lib/mako/pyparser.py
index b16672d6..5c55505b 100644
--- a/lib/mako/pyparser.py
+++ b/lib/mako/pyparser.py
@@ -1,5 +1,5 @@
# mako/pyparser.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -18,22 +18,13 @@ from mako import _ast_util
from mako import compat
from mako import exceptions
from mako import util
-from mako.compat import arg_stringname
-if compat.py3k:
- # words that cannot be assigned to (notably
- # smaller than the total keys in __builtins__)
- reserved = set(["True", "False", "None", "print"])
+# words that cannot be assigned to (notably
+# smaller than the total keys in __builtins__)
+reserved = {"True", "False", "None", "print"}
- # the "id" attribute on a function node
- arg_id = operator.attrgetter("arg")
-else:
- # words that cannot be assigned to (notably
- # smaller than the total keys in __builtins__)
- reserved = set(["True", "False", "None"])
-
- # the "id" attribute on a function node
- arg_id = operator.attrgetter("id")
+# the "id" attribute on a function node
+arg_id = operator.attrgetter("arg")
util.restore__ast(_ast)
@@ -43,7 +34,7 @@ def parse(code, mode="exec", **exception_kwargs):
try:
return _ast_util.parse(code, "", mode)
- except Exception:
+ except Exception as e:
raise exceptions.SyntaxException(
"(%s) %s (%r)"
% (
@@ -51,8 +42,8 @@ def parse(code, mode="exec", **exception_kwargs):
compat.exception_as(),
code[0:50],
),
- **exception_kwargs
- )
+ **exception_kwargs,
+ ) from e
class FindIdentifiers(_ast_util.NodeVisitor):
@@ -85,18 +76,13 @@ class FindIdentifiers(_ast_util.NodeVisitor):
self.visit(n)
self.in_assign_targets = in_a
- if compat.py3k:
-
- # ExceptHandler is in Python 2, but this block only works in
- # Python 3 (and is required there)
-
- def visit_ExceptHandler(self, node):
- if node.name is not None:
- self._add_declared(node.name)
- if node.type is not None:
- self.visit(node.type)
- for statement in node.body:
- self.visit(statement)
+ def visit_ExceptHandler(self, node):
+ if node.name is not None:
+ self._add_declared(node.name)
+ if node.type is not None:
+ self.visit(node.type)
+ for statement in node.body:
+ self.visit(statement)
def visit_Lambda(self, node, *args):
self._visit_function(node, True)
@@ -108,8 +94,7 @@ class FindIdentifiers(_ast_util.NodeVisitor):
def _expand_tuples(self, args):
for arg in args:
if isinstance(arg, _ast.Tuple):
- for n in arg.elts:
- yield n
+ yield from arg.elts
else:
yield arg
@@ -170,15 +155,15 @@ class FindIdentifiers(_ast_util.NodeVisitor):
for name in node.names:
if name.asname is not None:
self._add_declared(name.asname)
+ elif name.name == "*":
+ raise exceptions.CompileException(
+ "'import *' is not supported, since all identifier "
+ "names must be explicitly declared. Please use the "
+ "form 'from import , , "
+ "...' instead.",
+ **self.exception_kwargs,
+ )
else:
- if name.name == "*":
- raise exceptions.CompileException(
- "'import *' is not supported, since all identifier "
- "names must be explicitly declared. Please use the "
- "form 'from import , , "
- "...' instead.",
- **self.exception_kwargs
- )
self._add_declared(name.name)
@@ -213,27 +198,20 @@ class ParseFunc(_ast_util.NodeVisitor):
argnames = [arg_id(arg) for arg in node.args.args]
if node.args.vararg:
- argnames.append(arg_stringname(node.args.vararg))
+ argnames.append(node.args.vararg.arg)
- if compat.py2k:
- # kw-only args don't exist in Python 2
- kwargnames = []
- else:
- kwargnames = [arg_id(arg) for arg in node.args.kwonlyargs]
+ kwargnames = [arg_id(arg) for arg in node.args.kwonlyargs]
if node.args.kwarg:
- kwargnames.append(arg_stringname(node.args.kwarg))
+ kwargnames.append(node.args.kwarg.arg)
self.listener.argnames = argnames
self.listener.defaults = node.args.defaults # ast
self.listener.kwargnames = kwargnames
- if compat.py2k:
- self.listener.kwdefaults = []
- else:
- self.listener.kwdefaults = node.args.kw_defaults
+ self.listener.kwdefaults = node.args.kw_defaults
self.listener.varargs = node.args.vararg
self.listener.kwargs = node.args.kwarg
-class ExpressionGenerator(object):
+class ExpressionGenerator:
def __init__(self, astnode):
self.generator = _ast_util.SourceGenerator(" " * 4)
self.generator.visit(astnode)
diff --git a/lib/mako/runtime.py b/lib/mako/runtime.py
index 465908e6..6d7fa684 100644
--- a/lib/mako/runtime.py
+++ b/lib/mako/runtime.py
@@ -7,16 +7,16 @@
"""provides runtime services for templates, including Context,
Namespace, and various helper functions."""
+import builtins
import functools
import sys
from mako import compat
from mako import exceptions
from mako import util
-from mako.compat import compat_builtins
-class Context(object):
+class Context:
"""Provides runtime namespace, output buffer, and various
callstacks for templates.
@@ -24,7 +24,7 @@ class Context(object):
See :ref:`runtime_toplevel` for detail on the usage of
:class:`.Context`.
- """
+ """
def __init__(self, buffer, **data):
self._buffer_stack = [buffer]
@@ -103,7 +103,7 @@ class Context(object):
if key in self._data:
return self._data[key]
else:
- return compat_builtins.__dict__[key]
+ return builtins.__dict__[key]
def _push_writer(self):
"""push a capturing buffer onto this Context and return
@@ -135,7 +135,7 @@ class Context(object):
def get(self, key, default=None):
"""Return a value from this :class:`.Context`."""
- return self._data.get(key, compat_builtins.__dict__.get(key, default))
+ return self._data.get(key, builtins.__dict__.get(key, default))
def write(self, string):
"""Write a string to this :class:`.Context` object's
@@ -216,7 +216,7 @@ class CallerStack(list):
self.nextcaller = self.pop()
-class Undefined(object):
+class Undefined:
"""Represents an undefined value in a template.
@@ -240,7 +240,7 @@ UNDEFINED = Undefined()
STOP_RENDERING = ""
-class LoopStack(object):
+class LoopStack:
"""a stack for LoopContexts that implements the context manager protocol
to automatically pop off the top of the stack on context exit
@@ -280,7 +280,7 @@ class LoopStack(object):
return iter(self._top)
-class LoopContext(object):
+class LoopContext:
"""A magic loop variable.
Automatically accessible in any ``% for`` block.
@@ -339,14 +339,13 @@ class LoopContext(object):
return bool(self.index % 2)
def cycle(self, *values):
- """Cycle through values as the loop progresses.
- """
+ """Cycle through values as the loop progresses."""
if not values:
raise ValueError("You must provide values to cycle through")
return values[self.index % len(values)]
-class _NSAttr(object):
+class _NSAttr:
def __init__(self, parent):
self.__parent = parent
@@ -360,22 +359,22 @@ class _NSAttr(object):
raise AttributeError(key)
-class Namespace(object):
+class Namespace:
"""Provides access to collections of rendering methods, which
- can be local, from other templates, or from imported modules.
+ can be local, from other templates, or from imported modules.
- To access a particular rendering method referenced by a
- :class:`.Namespace`, use plain attribute access:
+ To access a particular rendering method referenced by a
+ :class:`.Namespace`, use plain attribute access:
- .. sourcecode:: mako
+ .. sourcecode:: mako
- ${some_namespace.foo(x, y, z)}
+ ${some_namespace.foo(x, y, z)}
- :class:`.Namespace` also contains several built-in attributes
- described here.
+ :class:`.Namespace` also contains several built-in attributes
+ described here.
- """
+ """
def __init__(
self,
@@ -390,7 +389,7 @@ class Namespace(object):
self.context = context
self.inherits = inherits
if callables is not None:
- self.callables = dict([(c.__name__, c) for c in callables])
+ self.callables = {c.__name__: c for c in callables}
callables = ()
@@ -482,15 +481,14 @@ class Namespace(object):
key = (self, uri)
if key in self.context.namespaces:
return self.context.namespaces[key]
- else:
- ns = TemplateNamespace(
- uri,
- self.context._copy(),
- templateuri=uri,
- calling_uri=self._templateuri,
- )
- self.context.namespaces[key] = ns
- return ns
+ ns = TemplateNamespace(
+ uri,
+ self.context._copy(),
+ templateuri=uri,
+ calling_uri=self._templateuri,
+ )
+ self.context.namespaces[key] = ns
+ return ns
def get_template(self, uri):
"""Return a :class:`.Template` from the given ``uri``.
@@ -574,7 +572,7 @@ class TemplateNamespace(Namespace):
self.context = context
self.inherits = inherits
if callables is not None:
- self.callables = dict([(c.__name__, c) for c in callables])
+ self.callables = {c.__name__: c for c in callables}
if templateuri is not None:
self.template = _lookup_template(context, templateuri, calling_uri)
@@ -666,7 +664,7 @@ class ModuleNamespace(Namespace):
self.context = context
self.inherits = inherits
if callables is not None:
- self.callables = dict([(c.__name__, c) for c in callables])
+ self.callables = {c.__name__: c for c in callables}
mod = __import__(module)
for token in module.split(".")[1:]:
@@ -790,7 +788,7 @@ def _include_file(context, uri, calling_uri, **kwargs):
except Exception:
result = template.include_error_handler(ctx, compat.exception_as())
if not result:
- compat.reraise(*sys.exc_info())
+ raise
else:
callable_(ctx, **kwargs)
@@ -837,8 +835,10 @@ def _lookup_template(context, uri, relativeto):
uri = lookup.adjust_uri(uri, relativeto)
try:
return lookup.get_template(uri)
- except exceptions.TopLevelLookupException:
- raise exceptions.TemplateLookupException(str(compat.exception_as()))
+ except exceptions.TopLevelLookupException as e:
+ raise exceptions.TemplateLookupException(
+ str(compat.exception_as())
+ ) from e
def _populate_self_namespace(context, template, self_ns=None):
@@ -862,14 +862,10 @@ def _render(template, callable_, args, data, as_unicode=False):
output of the given template and template callable."""
if as_unicode:
- buf = util.FastEncodingBuffer(as_unicode=True)
- elif template.bytestring_passthrough:
- buf = compat.StringIO()
+ buf = util.FastEncodingBuffer()
else:
buf = util.FastEncodingBuffer(
- as_unicode=as_unicode,
- encoding=template.output_encoding,
- errors=template.encoding_errors,
+ encoding=template.output_encoding, errors=template.encoding_errors
)
context = Context(buf, **data)
context._outputting_as_unicode = as_unicode
@@ -880,7 +876,7 @@ def _render(template, callable_, args, data, as_unicode=False):
callable_,
context,
*args,
- **_kwargs_for_callable(callable_, data)
+ **_kwargs_for_callable(callable_, data),
)
return context._pop_buffer().getvalue()
@@ -951,13 +947,15 @@ def _render_error(template, context, error):
if template.error_handler:
result = template.error_handler(context, error)
if not result:
- compat.reraise(*sys.exc_info())
+ tp, value, tb = sys.exc_info()
+ if value and tb:
+ raise value.with_traceback(tb)
+ else:
+ raise error
else:
error_template = exceptions.html_error_template()
if context._outputting_as_unicode:
- context._buffer_stack[:] = [
- util.FastEncodingBuffer(as_unicode=True)
- ]
+ context._buffer_stack[:] = [util.FastEncodingBuffer()]
else:
context._buffer_stack[:] = [
util.FastEncodingBuffer(
diff --git a/lib/mako/template.py b/lib/mako/template.py
index 5ed23204..bbbe73cb 100644
--- a/lib/mako/template.py
+++ b/lib/mako/template.py
@@ -1,5 +1,5 @@
# mako/template.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -25,7 +25,7 @@ from mako import util
from mako.lexer import Lexer
-class Template(object):
+class Template:
r"""Represents a compiled template.
@@ -53,17 +53,6 @@ class Template(object):
of return-valued ``%def``\ s "opt out" of that filtering via
passing special attributes or objects.
- :param bytestring_passthrough: When ``True``, and ``output_encoding`` is
- set to ``None``, and :meth:`.Template.render` is used to render,
- the `StringIO` or `cStringIO` buffer will be used instead of the
- default "fast" buffer. This allows raw bytestrings in the
- output stream, such as in expressions, to pass straight
- through to the buffer. This flag is forced
- to ``True`` if ``disable_unicode`` is also configured.
-
- .. versionadded:: 0.4
- Added to provide the same behavior as that of the previous series.
-
:param cache_args: Dictionary of cache configuration arguments that
will be passed to the :class:`.CacheImpl`. See :ref:`caching_toplevel`.
@@ -94,9 +83,6 @@ class Template(object):
:param default_filters: List of string filter names that will
be applied to all expressions. See :ref:`filtering_default_filters`.
- :param disable_unicode: Disables all awareness of Python Unicode
- objects. See :ref:`unicode_disabled`.
-
:param enable_loop: When ``True``, enable the ``loop`` context variable.
This can be set to ``False`` to support templates that may
be making usage of the name "``loop``". Individual templates can
@@ -255,9 +241,7 @@ class Template(object):
cache_url=None,
module_filename=None,
input_encoding=None,
- disable_unicode=False,
module_writer=None,
- bytestring_passthrough=False,
default_filters=None,
buffer_filters=(),
strict_undefined=False,
@@ -294,26 +278,12 @@ class Template(object):
self.input_encoding = input_encoding
self.output_encoding = output_encoding
self.encoding_errors = encoding_errors
- self.disable_unicode = disable_unicode
- self.bytestring_passthrough = bytestring_passthrough or disable_unicode
self.enable_loop = enable_loop
self.strict_undefined = strict_undefined
self.module_writer = module_writer
- if compat.py3k and disable_unicode:
- raise exceptions.UnsupportedError(
- "Mako for Python 3 does not " "support disabling Unicode"
- )
- elif output_encoding and disable_unicode:
- raise exceptions.UnsupportedError(
- "output_encoding must be set to "
- "None when disable_unicode is used."
- )
if default_filters is None:
- if compat.py3k or self.disable_unicode:
- self.default_filters = ["str"]
- else:
- self.default_filters = ["unicode"]
+ self.default_filters = ["str"]
else:
self.default_filters = default_filters
self.buffer_filters = buffer_filters
@@ -387,11 +357,7 @@ class Template(object):
):
self.cache_impl = cache_impl
self.cache_enabled = cache_enabled
- if cache_args:
- self.cache_args = cache_args
- else:
- self.cache_args = {}
-
+ self.cache_args = cache_args or {}
# transfer deprecated cache_* args
if cache_type:
self.cache_args["type"] = cache_type
@@ -463,7 +429,7 @@ class Template(object):
If the template specifies an output encoding, the string
will be encoded accordingly, else the output is raw (raw
- output uses `cStringIO` and can't handle multibyte
+ output uses `StringIO` and can't handle multibyte
characters). A :class:`.Context` object is created corresponding
to the given data. Arguments that are explicitly declared
by this template's internal rendering method are also
@@ -517,17 +483,17 @@ class ModuleTemplate(Template):
"""A Template which is constructed given an existing Python module.
- e.g.::
+ e.g.::
- t = Template("this is a template")
- f = file("mymodule.py", "w")
- f.write(t.code)
- f.close()
+ t = Template("this is a template")
+ f = file("mymodule.py", "w")
+ f.write(t.code)
+ f.close()
- import mymodule
+ import mymodule
- t = ModuleTemplate(mymodule)
- print(t.render())
+ t = ModuleTemplate(mymodule)
+ print(t.render())
"""
@@ -541,8 +507,6 @@ class ModuleTemplate(Template):
template_source=None,
output_encoding=None,
encoding_errors="strict",
- disable_unicode=False,
- bytestring_passthrough=False,
format_exceptions=False,
error_handler=None,
lookup=None,
@@ -559,20 +523,8 @@ class ModuleTemplate(Template):
self.input_encoding = module._source_encoding
self.output_encoding = output_encoding
self.encoding_errors = encoding_errors
- self.disable_unicode = disable_unicode
- self.bytestring_passthrough = bytestring_passthrough or disable_unicode
self.enable_loop = module._enable_loop
- if compat.py3k and disable_unicode:
- raise exceptions.UnsupportedError(
- "Mako for Python 3 does not " "support disabling Unicode"
- )
- elif output_encoding and disable_unicode:
- raise exceptions.UnsupportedError(
- "output_encoding must be set to "
- "None when disable_unicode is used."
- )
-
self.module = module
self.filename = template_filename
ModuleInfo(
@@ -616,19 +568,18 @@ class DefTemplate(Template):
self.include_error_handler = parent.include_error_handler
self.enable_loop = parent.enable_loop
self.lookup = parent.lookup
- self.bytestring_passthrough = parent.bytestring_passthrough
def get_def(self, name):
return self.parent.get_def(name)
-class ModuleInfo(object):
+class ModuleInfo:
"""Stores information about a module currently loaded into
memory, provides reverse lookups of template source, module
source code based on a module's identifier.
- """
+ """
_modules = weakref.WeakValueDictionary()
@@ -658,9 +609,9 @@ class ModuleInfo(object):
r"__M_BEGIN_METADATA(.+?)__M_END_METADATA", module_source, re.S
).group(1)
source_map = json.loads(source_map)
- source_map["line_map"] = dict(
- (int(k), int(v)) for k, v in source_map["line_map"].items()
- )
+ source_map["line_map"] = {
+ int(k): int(v) for k, v in source_map["line_map"].items()
+ }
if full_line_map:
f_line_map = source_map["full_line_map"] = []
line_map = source_map["line_map"]
@@ -681,28 +632,25 @@ class ModuleInfo(object):
@property
def source(self):
- if self.template_source is not None:
- if self.module._source_encoding and not isinstance(
- self.template_source, compat.text_type
- ):
- return self.template_source.decode(
- self.module._source_encoding
- )
- else:
- return self.template_source
- else:
+ if self.template_source is None:
data = util.read_file(self.template_filename)
if self.module._source_encoding:
return data.decode(self.module._source_encoding)
else:
return data
+ elif self.module._source_encoding and not isinstance(
+ self.template_source, str
+ ):
+ return self.template_source.decode(self.module._source_encoding)
+ else:
+ return self.template_source
+
def _compile(template, text, filename, generate_magic_comment):
lexer = template.lexer_cls(
text,
filename,
- disable_unicode=template.disable_unicode,
input_encoding=template.input_encoding,
preprocessor=template.preprocessor,
)
@@ -717,7 +665,6 @@ def _compile(template, text, filename, generate_magic_comment):
future_imports=template.future_imports,
source_encoding=lexer.encoding,
generate_magic_comment=generate_magic_comment,
- disable_unicode=template.disable_unicode,
strict_undefined=template.strict_undefined,
enable_loop=template.enable_loop,
reserved_names=template.reserved_names,
@@ -728,15 +675,10 @@ def _compile(template, text, filename, generate_magic_comment):
def _compile_text(template, text, filename):
identifier = template.module_id
source, lexer = _compile(
- template,
- text,
- filename,
- generate_magic_comment=template.disable_unicode,
+ template, text, filename, generate_magic_comment=False
)
cid = identifier
- if not compat.py3k and isinstance(cid, compat.text_type):
- cid = cid.encode()
module = types.ModuleType(cid)
code = compile(source, cid, "exec")
@@ -750,7 +692,7 @@ def _compile_module_file(template, text, filename, outputpath, module_writer):
template, text, filename, generate_magic_comment=True
)
- if isinstance(source, compat.text_type):
+ if isinstance(source, str):
source = source.encode(lexer.encoding or "ascii")
if module_writer:
@@ -767,10 +709,7 @@ def _compile_module_file(template, text, filename, outputpath, module_writer):
def _get_module_info_from_callable(callable_):
- if compat.py3k:
- return _get_module_info(callable_.__globals__["__name__"])
- else:
- return _get_module_info(callable_.func_globals["__name__"])
+ return _get_module_info(callable_.__globals__["__name__"])
def _get_module_info(filename):
diff --git a/lib/mako/testing/__init__.py b/lib/mako/testing/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/mako/testing/_config.py b/lib/mako/testing/_config.py
new file mode 100644
index 00000000..4ee3d0a6
--- /dev/null
+++ b/lib/mako/testing/_config.py
@@ -0,0 +1,128 @@
+import configparser
+import dataclasses
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Callable
+from typing import ClassVar
+from typing import Optional
+from typing import Union
+
+from .helpers import make_path
+
+
+class ConfigError(BaseException):
+ pass
+
+
+class MissingConfig(ConfigError):
+ pass
+
+
+class MissingConfigSection(ConfigError):
+ pass
+
+
+class MissingConfigItem(ConfigError):
+ pass
+
+
+class ConfigValueTypeError(ConfigError):
+ pass
+
+
+class _GetterDispatch:
+ def __init__(self, initialdata, default_getter: Callable):
+ self.default_getter = default_getter
+ self.data = initialdata
+
+ def get_fn_for_type(self, type_):
+ return self.data.get(type_, self.default_getter)
+
+ def get_typed_value(self, type_, name):
+ get_fn = self.get_fn_for_type(type_)
+ return get_fn(name)
+
+
+def _parse_cfg_file(filespec: Union[Path, str]):
+ cfg = configparser.ConfigParser()
+ try:
+ filepath = make_path(filespec, check_exists=True)
+ except FileNotFoundError as e:
+ raise MissingConfig(f"No config file found at {filespec}") from e
+ else:
+ with open(filepath, encoding="utf-8") as f:
+ cfg.read_file(f)
+ return cfg
+
+
+def _build_getter(cfg_obj, cfg_section, method, converter=None):
+ def caller(option, **kwargs):
+ try:
+ rv = getattr(cfg_obj, method)(cfg_section, option, **kwargs)
+ except configparser.NoSectionError as nse:
+ raise MissingConfigSection(
+ f"No config section named {cfg_section}"
+ ) from nse
+ except configparser.NoOptionError as noe:
+ raise MissingConfigItem(f"No config item for {option}") from noe
+ except ValueError as ve:
+ # ConfigParser.getboolean, .getint, .getfloat raise ValueError
+ # on bad types
+ raise ConfigValueTypeError(
+ f"Wrong value type for {option}"
+ ) from ve
+ else:
+ if converter:
+ try:
+ rv = converter(rv)
+ except Exception as e:
+ raise ConfigValueTypeError(
+ f"Wrong value type for {option}"
+ ) from e
+ return rv
+
+ return caller
+
+
+def _build_getter_dispatch(cfg_obj, cfg_section, converters=None):
+ converters = converters or {}
+
+ default_getter = _build_getter(cfg_obj, cfg_section, "get")
+
+ # support ConfigParser builtins
+ getters = {
+ int: _build_getter(cfg_obj, cfg_section, "getint"),
+ bool: _build_getter(cfg_obj, cfg_section, "getboolean"),
+ float: _build_getter(cfg_obj, cfg_section, "getfloat"),
+ str: default_getter,
+ }
+
+ # use ConfigParser.get and convert value
+ getters.update(
+ {
+ type_: _build_getter(
+ cfg_obj, cfg_section, "get", converter=converter_fn
+ )
+ for type_, converter_fn in converters.items()
+ }
+ )
+
+ return _GetterDispatch(getters, default_getter)
+
+
+@dataclass
+class ReadsCfg:
+ section_header: ClassVar[str]
+ converters: ClassVar[Optional[dict]] = None
+
+ @classmethod
+ def from_cfg_file(cls, filespec: Union[Path, str]):
+ cfg = _parse_cfg_file(filespec)
+ dispatch = _build_getter_dispatch(
+ cfg, cls.section_header, converters=cls.converters
+ )
+ kwargs = {
+ field.name: dispatch.get_typed_value(field.type, field.name)
+ for field in dataclasses.fields(cls)
+ }
+ return cls(**kwargs)
diff --git a/lib/mako/testing/assertions.py b/lib/mako/testing/assertions.py
new file mode 100644
index 00000000..14ea6352
--- /dev/null
+++ b/lib/mako/testing/assertions.py
@@ -0,0 +1,167 @@
+import contextlib
+import re
+import sys
+
+
+def eq_(a, b, msg=None):
+ """Assert a == b, with repr messaging on failure."""
+ assert a == b, msg or "%r != %r" % (a, b)
+
+
+def ne_(a, b, msg=None):
+ """Assert a != b, with repr messaging on failure."""
+ assert a != b, msg or "%r == %r" % (a, b)
+
+
+def in_(a, b, msg=None):
+ """Assert a in b, with repr messaging on failure."""
+ assert a in b, msg or "%r not in %r" % (a, b)
+
+
+def not_in(a, b, msg=None):
+ """Assert a in not b, with repr messaging on failure."""
+ assert a not in b, msg or "%r is in %r" % (a, b)
+
+
+def _assert_proper_exception_context(exception):
+ """assert that any exception we're catching does not have a __context__
+ without a __cause__, and that __suppress_context__ is never set.
+
+ Python 3 will report nested as exceptions as "during the handling of
+ error X, error Y occurred". That's not what we want to do. We want
+ these exceptions in a cause chain.
+
+ """
+
+ if (
+ exception.__context__ is not exception.__cause__
+ and not exception.__suppress_context__
+ ):
+ assert False, (
+ "Exception %r was correctly raised but did not set a cause, "
+ "within context %r as its cause."
+ % (exception, exception.__context__)
+ )
+
+
+def _assert_proper_cause_cls(exception, cause_cls):
+ """assert that any exception we're catching does not have a __context__
+ without a __cause__, and that __suppress_context__ is never set.
+
+ Python 3 will report nested as exceptions as "during the handling of
+ error X, error Y occurred". That's not what we want to do. We want
+ these exceptions in a cause chain.
+
+ """
+ assert isinstance(exception.__cause__, cause_cls), (
+ "Exception %r was correctly raised but has cause %r, which does not "
+ "have the expected cause type %r."
+ % (exception, exception.__cause__, cause_cls)
+ )
+
+
+def assert_raises(except_cls, callable_, *args, **kw):
+ return _assert_raises(except_cls, callable_, args, kw)
+
+
+def assert_raises_with_proper_context(except_cls, callable_, *args, **kw):
+ return _assert_raises(except_cls, callable_, args, kw, check_context=True)
+
+
+def assert_raises_with_given_cause(
+ except_cls, cause_cls, callable_, *args, **kw
+):
+ return _assert_raises(except_cls, callable_, args, kw, cause_cls=cause_cls)
+
+
+def assert_raises_message(except_cls, msg, callable_, *args, **kwargs):
+ return _assert_raises(except_cls, callable_, args, kwargs, msg=msg)
+
+
+def assert_raises_message_with_proper_context(
+ except_cls, msg, callable_, *args, **kwargs
+):
+ return _assert_raises(
+ except_cls, callable_, args, kwargs, msg=msg, check_context=True
+ )
+
+
+def assert_raises_message_with_given_cause(
+ except_cls, msg, cause_cls, callable_, *args, **kwargs
+):
+ return _assert_raises(
+ except_cls, callable_, args, kwargs, msg=msg, cause_cls=cause_cls
+ )
+
+
+def _assert_raises(
+ except_cls,
+ callable_,
+ args,
+ kwargs,
+ msg=None,
+ check_context=False,
+ cause_cls=None,
+):
+
+ with _expect_raises(except_cls, msg, check_context, cause_cls) as ec:
+ callable_(*args, **kwargs)
+ return ec.error
+
+
+class _ErrorContainer:
+ error = None
+
+
+@contextlib.contextmanager
+def _expect_raises(except_cls, msg=None, check_context=False, cause_cls=None):
+ ec = _ErrorContainer()
+ if check_context:
+ are_we_already_in_a_traceback = sys.exc_info()[0]
+ try:
+ yield ec
+ success = False
+ except except_cls as err:
+ ec.error = err
+ success = True
+ if msg is not None:
+ # I'm often pdbing here, and "err" above isn't
+ # in scope, so assign the string explicitly
+ error_as_string = str(err)
+ assert re.search(msg, error_as_string, re.UNICODE), "%r !~ %s" % (
+ msg,
+ error_as_string,
+ )
+ if cause_cls is not None:
+ _assert_proper_cause_cls(err, cause_cls)
+ if check_context and not are_we_already_in_a_traceback:
+ _assert_proper_exception_context(err)
+ print(str(err).encode("utf-8"))
+
+ # it's generally a good idea to not carry traceback objects outside
+ # of the except: block, but in this case especially we seem to have
+ # hit some bug in either python 3.10.0b2 or greenlet or both which
+ # this seems to fix:
+ # https://github.com/python-greenlet/greenlet/issues/242
+ del ec
+
+ # assert outside the block so it works for AssertionError too !
+ assert success, "Callable did not raise an exception"
+
+
+def expect_raises(except_cls, check_context=False):
+ return _expect_raises(except_cls, check_context=check_context)
+
+
+def expect_raises_message(except_cls, msg, check_context=False):
+ return _expect_raises(except_cls, msg=msg, check_context=check_context)
+
+
+def expect_raises_with_proper_context(except_cls, check_context=True):
+ return _expect_raises(except_cls, check_context=check_context)
+
+
+def expect_raises_message_with_proper_context(
+ except_cls, msg, check_context=True
+):
+ return _expect_raises(except_cls, msg=msg, check_context=check_context)
diff --git a/lib/mako/testing/config.py b/lib/mako/testing/config.py
new file mode 100644
index 00000000..b77d0c08
--- /dev/null
+++ b/lib/mako/testing/config.py
@@ -0,0 +1,17 @@
+from dataclasses import dataclass
+from pathlib import Path
+
+from ._config import ReadsCfg
+from .helpers import make_path
+
+
+@dataclass
+class Config(ReadsCfg):
+ module_base: Path
+ template_base: Path
+
+ section_header = "mako_testing"
+ converters = {Path: make_path}
+
+
+config = Config.from_cfg_file("./setup.cfg")
diff --git a/lib/mako/testing/exclusions.py b/lib/mako/testing/exclusions.py
new file mode 100644
index 00000000..37b2d14a
--- /dev/null
+++ b/lib/mako/testing/exclusions.py
@@ -0,0 +1,80 @@
+import pytest
+
+from mako.ext.beaker_cache import has_beaker
+from mako.util import update_wrapper
+
+
+try:
+ import babel.messages.extract as babel
+except ImportError:
+ babel = None
+
+
+try:
+ import lingua
+except ImportError:
+ lingua = None
+
+
+try:
+ import dogpile.cache # noqa
+except ImportError:
+ has_dogpile_cache = False
+else:
+ has_dogpile_cache = True
+
+
+requires_beaker = pytest.mark.skipif(
+ not has_beaker, reason="Beaker is required for these tests."
+)
+
+
+requires_babel = pytest.mark.skipif(
+ babel is None, reason="babel not installed: skipping babelplugin test"
+)
+
+
+requires_lingua = pytest.mark.skipif(
+ lingua is None, reason="lingua not installed: skipping linguaplugin test"
+)
+
+
+requires_dogpile_cache = pytest.mark.skipif(
+ not has_dogpile_cache,
+ reason="dogpile.cache is required to run these tests",
+)
+
+
+def _pygments_version():
+ try:
+ import pygments
+
+ version = pygments.__version__
+ except:
+ version = "0"
+ return version
+
+
+requires_pygments_14 = pytest.mark.skipif(
+ _pygments_version() < "1.4", reason="Requires pygments 1.4 or greater"
+)
+
+
+# def requires_pygments_14(fn):
+
+# return skip_if(
+# lambda: version < "1.4", "Requires pygments 1.4 or greater"
+# )(fn)
+
+
+def requires_no_pygments_exceptions(fn):
+ def go(*arg, **kw):
+ from mako import exceptions
+
+ exceptions._install_fallback()
+ try:
+ return fn(*arg, **kw)
+ finally:
+ exceptions._install_highlighting()
+
+ return update_wrapper(go, fn)
diff --git a/lib/mako/testing/fixtures.py b/lib/mako/testing/fixtures.py
new file mode 100644
index 00000000..c9379c0c
--- /dev/null
+++ b/lib/mako/testing/fixtures.py
@@ -0,0 +1,109 @@
+import os
+
+from mako.cache import CacheImpl
+from mako.cache import register_plugin
+from mako.template import Template
+from .assertions import eq_
+from .config import config
+
+
+class TemplateTest:
+ def _file_template(self, filename, **kw):
+ filepath = self._file_path(filename)
+ return Template(
+ uri=filename,
+ filename=filepath,
+ module_directory=config.module_base,
+ **kw,
+ )
+
+ def _file_path(self, filename):
+ name, ext = os.path.splitext(filename)
+ py3k_path = os.path.join(config.template_base, name + "_py3k" + ext)
+ if os.path.exists(py3k_path):
+ return py3k_path
+
+ return os.path.join(config.template_base, filename)
+
+ def _do_file_test(
+ self,
+ filename,
+ expected,
+ filters=None,
+ unicode_=True,
+ template_args=None,
+ **kw,
+ ):
+ t1 = self._file_template(filename, **kw)
+ self._do_test(
+ t1,
+ expected,
+ filters=filters,
+ unicode_=unicode_,
+ template_args=template_args,
+ )
+
+ def _do_memory_test(
+ self,
+ source,
+ expected,
+ filters=None,
+ unicode_=True,
+ template_args=None,
+ **kw,
+ ):
+ t1 = Template(text=source, **kw)
+ self._do_test(
+ t1,
+ expected,
+ filters=filters,
+ unicode_=unicode_,
+ template_args=template_args,
+ )
+
+ def _do_test(
+ self,
+ template,
+ expected,
+ filters=None,
+ template_args=None,
+ unicode_=True,
+ ):
+ if template_args is None:
+ template_args = {}
+ if unicode_:
+ output = template.render_unicode(**template_args)
+ else:
+ output = template.render(**template_args)
+
+ if filters:
+ output = filters(output)
+ eq_(output, expected)
+
+
+class PlainCacheImpl(CacheImpl):
+ """Simple memory cache impl so that tests which
+ use caching can run without beaker."""
+
+ def __init__(self, cache):
+ self.cache = cache
+ self.data = {}
+
+ def get_or_create(self, key, creation_function, **kw):
+ if key in self.data:
+ return self.data[key]
+ else:
+ self.data[key] = data = creation_function(**kw)
+ return data
+
+ def put(self, key, value, **kw):
+ self.data[key] = value
+
+ def get(self, key, **kw):
+ return self.data[key]
+
+ def invalidate(self, key, **kw):
+ del self.data[key]
+
+
+register_plugin("plain", __name__, "PlainCacheImpl")
diff --git a/lib/mako/testing/helpers.py b/lib/mako/testing/helpers.py
new file mode 100644
index 00000000..77cca367
--- /dev/null
+++ b/lib/mako/testing/helpers.py
@@ -0,0 +1,67 @@
+import contextlib
+import pathlib
+from pathlib import Path
+import re
+import time
+from typing import Union
+from unittest import mock
+
+
+def flatten_result(result):
+ return re.sub(r"[\s\r\n]+", " ", result).strip()
+
+
+def result_lines(result):
+ return [
+ x.strip()
+ for x in re.split(r"\r?\n", re.sub(r" +", " ", result))
+ if x.strip() != ""
+ ]
+
+
+def make_path(
+ filespec: Union[Path, str],
+ make_absolute: bool = True,
+ check_exists: bool = False,
+) -> Path:
+ path = Path(filespec)
+ if make_absolute:
+ path = path.resolve(strict=check_exists)
+ if check_exists and (not path.exists()):
+ raise FileNotFoundError(f"No file or directory at {filespec}")
+ return path
+
+
+def _unlink_path(path, missing_ok=False):
+ # Replicate 3.8+ functionality in 3.7
+ cm = contextlib.nullcontext()
+ if missing_ok:
+ cm = contextlib.suppress(FileNotFoundError)
+
+ with cm:
+ path.unlink()
+
+
+def replace_file_with_dir(pathspec):
+ path = pathlib.Path(pathspec)
+ _unlink_path(path, missing_ok=True)
+ path.mkdir(exist_ok=True)
+ return path
+
+
+def file_with_template_code(filespec):
+ with open(filespec, "w") as f:
+ f.write(
+ """
+i am an artificial template just for you
+"""
+ )
+ return filespec
+
+
+@contextlib.contextmanager
+def rewind_compile_time(hours=1):
+ rewound = time.time() - (hours * 3_600)
+ with mock.patch("mako.codegen.time") as codegen_time:
+ codegen_time.time.return_value = rewound
+ yield
diff --git a/lib/mako/util.py b/lib/mako/util.py
index 16e3c726..74c8b9eb 100644
--- a/lib/mako/util.py
+++ b/lib/mako/util.py
@@ -1,10 +1,8 @@
# mako/util.py
-# Copyright 2006-2020 the Mako authors and contributors
+# Copyright 2006-2021 the Mako authors and contributors
#
# This module is part of Mako and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from __future__ import absolute_import
-
from ast import parse
import codecs
import collections
@@ -13,7 +11,7 @@ import os
import re
import timeit
-from mako import compat
+from .compat import importlib_metadata_get
def update_wrapper(decorated, fn):
@@ -22,7 +20,7 @@ def update_wrapper(decorated, fn):
return decorated
-class PluginLoader(object):
+class PluginLoader:
def __init__(self, group):
self.group = group
self.impls = {}
@@ -30,18 +28,17 @@ class PluginLoader(object):
def load(self, name):
if name in self.impls:
return self.impls[name]()
- else:
- import pkg_resources
- for impl in pkg_resources.iter_entry_points(self.group, name):
+ for impl in importlib_metadata_get(self.group):
+ if impl.name == name:
self.impls[name] = impl.load
return impl.load()
- else:
- from mako import exceptions
- raise exceptions.RuntimeException(
- "Can't load plugin %s %s" % (self.group, name)
- )
+ from mako import exceptions
+
+ raise exceptions.RuntimeException(
+ "Can't load plugin %s %s" % (self.group, name)
+ )
def register(self, name, modulepath, objname):
def load():
@@ -61,7 +58,7 @@ def verify_directory(dir_):
while not os.path.exists(dir_):
try:
tries += 1
- os.makedirs(dir_, compat.octal("0775"))
+ os.makedirs(dir_, 0o755)
except:
if tries > 5:
raise
@@ -76,7 +73,7 @@ def to_list(x, default=None):
return x
-class memoized_property(object):
+class memoized_property:
"""A read-only @property that is only evaluated once."""
@@ -92,7 +89,7 @@ class memoized_property(object):
return result
-class memoized_instancemethod(object):
+class memoized_instancemethod:
"""Decorate a method memoize its return value.
@@ -140,19 +137,15 @@ class SetLikeDict(dict):
return x
-class FastEncodingBuffer(object):
+class FastEncodingBuffer:
"""a very rudimentary buffer that is faster than StringIO,
- but doesn't crash on unicode data like cStringIO."""
+ and supports unicode data."""
- def __init__(self, encoding=None, errors="strict", as_unicode=False):
+ def __init__(self, encoding=None, errors="strict"):
self.data = collections.deque()
self.encoding = encoding
- if as_unicode:
- self.delim = compat.u("")
- else:
- self.delim = ""
- self.as_unicode = as_unicode
+ self.delim = ""
self.errors = errors
self.write = self.data.append
@@ -179,7 +172,7 @@ class LRUCache(dict):
is inexact.
"""
- class _Item(object):
+ class _Item:
def __init__(self, key, value):
self.key = key
self.value = value
@@ -203,9 +196,8 @@ class LRUCache(dict):
def setdefault(self, key, value):
if key in self:
return self[key]
- else:
- self[key] = value
- return value
+ self[key] = value
+ return value
def __setitem__(self, key, value):
item = dict.get(self, key)
@@ -295,7 +287,7 @@ def sorted_dict_repr(d):
"""
keys = list(d.keys())
keys.sort()
- return "{" + ", ".join(["%r: %r" % (k, d[k]) for k in keys]) + "}"
+ return "{" + ", ".join("%r: %r" % (k, d[k]) for k in keys) + "}"
def restore__ast(_ast):
@@ -308,7 +300,7 @@ def restore__ast(_ast):
m = compile(
"""\
def foo(): pass
-class Bar(object): pass
+class Bar: pass
if False: pass
baz = 'mako'
1 + 2 - 3 * 4 / 5
@@ -380,12 +372,8 @@ mako in baz not in mako""",
def read_file(path, mode="rb"):
- fp = open(path, mode)
- try:
- data = fp.read()
- return data
- finally:
- fp.close()
+ with open(path, mode) as fp:
+ return fp.read()
def read_python_file(path):
diff --git a/lib/markupsafe/__init__.py b/lib/markupsafe/__init__.py
index d331ac36..0f1c4f46 100644
--- a/lib/markupsafe/__init__.py
+++ b/lib/markupsafe/__init__.py
@@ -11,9 +11,10 @@ if t.TYPE_CHECKING:
pass
-__version__ = "2.0.1"
+__version__ = "2.1.1"
-_striptags_re = re.compile(r"(|<[^>]*>)")
+_strip_comments_re = re.compile(r"")
+_strip_tags_re = re.compile(r"<.*?>")
def _simple_escaping_wrapper(name: str) -> t.Callable[..., "Markup"]:
@@ -92,19 +93,24 @@ class Markup(str):
return NotImplemented
- def __mul__(self, num: int) -> "Markup":
+ def __mul__(self, num: "te.SupportsIndex") -> "Markup":
if isinstance(num, int):
return self.__class__(super().__mul__(num))
- return NotImplemented # type: ignore
+ return NotImplemented
__rmul__ = __mul__
def __mod__(self, arg: t.Any) -> "Markup":
if isinstance(arg, tuple):
+ # a tuple of arguments, each wrapped
arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
- else:
+ elif hasattr(type(arg), "__getitem__") and not isinstance(arg, str):
+ # a mapping of arguments, wrapped
arg = _MarkupEscapeHelper(arg, self.escape)
+ else:
+ # a single argument, wrapped with the helper and a tuple
+ arg = (_MarkupEscapeHelper(arg, self.escape),)
return self.__class__(super().__mod__(arg))
@@ -153,8 +159,11 @@ class Markup(str):
>>> Markup("Main »\tAbout").striptags()
'Main ยป About'
"""
- stripped = " ".join(_striptags_re.sub("", self).split())
- return Markup(stripped).unescape()
+ # Use two regexes to avoid ambiguous matches.
+ value = _strip_comments_re.sub("", self)
+ value = _strip_tags_re.sub("", value)
+ value = " ".join(value.split())
+ return Markup(value).unescape()
@classmethod
def escape(cls, s: t.Any) -> "Markup":
@@ -280,9 +289,7 @@ try:
from ._speedups import escape as escape
from ._speedups import escape_silent as escape_silent
from ._speedups import soft_str as soft_str
- from ._speedups import soft_unicode
except ImportError:
from ._native import escape as escape
from ._native import escape_silent as escape_silent # noqa: F401
from ._native import soft_str as soft_str # noqa: F401
- from ._native import soft_unicode # noqa: F401
diff --git a/lib/markupsafe/_native.py b/lib/markupsafe/_native.py
index 6f7eb7a8..8117b271 100644
--- a/lib/markupsafe/_native.py
+++ b/lib/markupsafe/_native.py
@@ -61,15 +61,3 @@ def soft_str(s: t.Any) -> str:
return str(s)
return s
-
-
-def soft_unicode(s: t.Any) -> str:
- import warnings
-
- warnings.warn(
- "'soft_unicode' has been renamed to 'soft_str'. The old name"
- " will be removed in MarkupSafe 2.1.",
- DeprecationWarning,
- stacklevel=2,
- )
- return soft_str(s)
diff --git a/lib/markupsafe/_speedups.c b/lib/markupsafe/_speedups.c
new file mode 100644
index 00000000..3c463fb8
--- /dev/null
+++ b/lib/markupsafe/_speedups.c
@@ -0,0 +1,320 @@
+#include
+
+static PyObject* markup;
+
+static int
+init_constants(void)
+{
+ PyObject *module;
+
+ /* import markup type so that we can mark the return value */
+ module = PyImport_ImportModule("markupsafe");
+ if (!module)
+ return 0;
+ markup = PyObject_GetAttrString(module, "Markup");
+ Py_DECREF(module);
+
+ return 1;
+}
+
+#define GET_DELTA(inp, inp_end, delta) \
+ while (inp < inp_end) { \
+ switch (*inp++) { \
+ case '"': \
+ case '\'': \
+ case '&': \
+ delta += 4; \
+ break; \
+ case '<': \
+ case '>': \
+ delta += 3; \
+ break; \
+ } \
+ }
+
+#define DO_ESCAPE(inp, inp_end, outp) \
+ { \
+ Py_ssize_t ncopy = 0; \
+ while (inp < inp_end) { \
+ switch (*inp) { \
+ case '"': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '4'; \
+ *outp++ = ';'; \
+ break; \
+ case '\'': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = '#'; \
+ *outp++ = '3'; \
+ *outp++ = '9'; \
+ *outp++ = ';'; \
+ break; \
+ case '&': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'a'; \
+ *outp++ = 'm'; \
+ *outp++ = 'p'; \
+ *outp++ = ';'; \
+ break; \
+ case '<': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'l'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ case '>': \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ outp += ncopy; ncopy = 0; \
+ *outp++ = '&'; \
+ *outp++ = 'g'; \
+ *outp++ = 't'; \
+ *outp++ = ';'; \
+ break; \
+ default: \
+ ncopy++; \
+ } \
+ inp++; \
+ } \
+ memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \
+ }
+
+static PyObject*
+escape_unicode_kind1(PyUnicodeObject *in)
+{
+ Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
+ Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS1 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
+ PyUnicode_IS_ASCII(in) ? 127 : 255);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_1BYTE_DATA(in);
+ outp = PyUnicode_1BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode_kind2(PyUnicodeObject *in)
+{
+ Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
+ Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS2 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_2BYTE_DATA(in);
+ outp = PyUnicode_2BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+
+static PyObject*
+escape_unicode_kind4(PyUnicodeObject *in)
+{
+ Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
+ Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
+ Py_UCS4 *outp;
+ PyObject *out;
+ Py_ssize_t delta = 0;
+
+ GET_DELTA(inp, inp_end, delta);
+ if (!delta) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
+ if (!out)
+ return NULL;
+
+ inp = PyUnicode_4BYTE_DATA(in);
+ outp = PyUnicode_4BYTE_DATA(out);
+ DO_ESCAPE(inp, inp_end, outp);
+ return out;
+}
+
+static PyObject*
+escape_unicode(PyUnicodeObject *in)
+{
+ if (PyUnicode_READY(in))
+ return NULL;
+
+ switch (PyUnicode_KIND(in)) {
+ case PyUnicode_1BYTE_KIND:
+ return escape_unicode_kind1(in);
+ case PyUnicode_2BYTE_KIND:
+ return escape_unicode_kind2(in);
+ case PyUnicode_4BYTE_KIND:
+ return escape_unicode_kind4(in);
+ }
+ assert(0); /* shouldn't happen */
+ return NULL;
+}
+
+static PyObject*
+escape(PyObject *self, PyObject *text)
+{
+ static PyObject *id_html;
+ PyObject *s = NULL, *rv = NULL, *html;
+
+ if (id_html == NULL) {
+ id_html = PyUnicode_InternFromString("__html__");
+ if (id_html == NULL) {
+ return NULL;
+ }
+ }
+
+ /* we don't have to escape integers, bools or floats */
+ if (PyLong_CheckExact(text) ||
+ PyFloat_CheckExact(text) || PyBool_Check(text) ||
+ text == Py_None)
+ return PyObject_CallFunctionObjArgs(markup, text, NULL);
+
+ /* if the object has an __html__ method that performs the escaping */
+ html = PyObject_GetAttr(text ,id_html);
+ if (html) {
+ s = PyObject_CallObject(html, NULL);
+ Py_DECREF(html);
+ if (s == NULL) {
+ return NULL;
+ }
+ /* Convert to Markup object */
+ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
+ Py_DECREF(s);
+ return rv;
+ }
+
+ /* otherwise make the object unicode if it isn't, then escape */
+ PyErr_Clear();
+ if (!PyUnicode_Check(text)) {
+ PyObject *unicode = PyObject_Str(text);
+ if (!unicode)
+ return NULL;
+ s = escape_unicode((PyUnicodeObject*)unicode);
+ Py_DECREF(unicode);
+ }
+ else
+ s = escape_unicode((PyUnicodeObject*)text);
+
+ /* convert the unicode string into a markup object. */
+ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
+ Py_DECREF(s);
+ return rv;
+}
+
+
+static PyObject*
+escape_silent(PyObject *self, PyObject *text)
+{
+ if (text != Py_None)
+ return escape(self, text);
+ return PyObject_CallFunctionObjArgs(markup, NULL);
+}
+
+
+static PyObject*
+soft_str(PyObject *self, PyObject *s)
+{
+ if (!PyUnicode_Check(s))
+ return PyObject_Str(s);
+ Py_INCREF(s);
+ return s;
+}
+
+
+static PyMethodDef module_methods[] = {
+ {
+ "escape",
+ (PyCFunction)escape,
+ METH_O,
+ "Replace the characters ``&``, ``<``, ``>``, ``'``, and ``\"`` in"
+ " the string with HTML-safe sequences. Use this if you need to display"
+ " text that might contain such characters in HTML.\n\n"
+ "If the object has an ``__html__`` method, it is called and the"
+ " return value is assumed to already be safe for HTML.\n\n"
+ ":param s: An object to be converted to a string and escaped.\n"
+ ":return: A :class:`Markup` string with the escaped text.\n"
+ },
+ {
+ "escape_silent",
+ (PyCFunction)escape_silent,
+ METH_O,
+ "Like :func:`escape` but treats ``None`` as the empty string."
+ " Useful with optional values, as otherwise you get the string"
+ " ``'None'`` when the value is ``None``.\n\n"
+ ">>> escape(None)\n"
+ "Markup('None')\n"
+ ">>> escape_silent(None)\n"
+ "Markup('')\n"
+ },
+ {
+ "soft_str",
+ (PyCFunction)soft_str,
+ METH_O,
+ "Convert an object to a string if it isn't already. This preserves"
+ " a :class:`Markup` string rather than converting it back to a basic"
+ " string, so it will still be marked as safe and won't be escaped"
+ " again.\n\n"
+ ">>> value = escape(\"\")\n"
+ ">>> value\n"
+ "Markup('<User 1>')\n"
+ ">>> escape(str(value))\n"
+ "Markup('<User 1>')\n"
+ ">>> escape(soft_str(value))\n"
+ "Markup('<User 1>')\n"
+ },
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+static struct PyModuleDef module_definition = {
+ PyModuleDef_HEAD_INIT,
+ "markupsafe._speedups",
+ NULL,
+ -1,
+ module_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+PyMODINIT_FUNC
+PyInit__speedups(void)
+{
+ if (!init_constants())
+ return NULL;
+
+ return PyModule_Create(&module_definition);
+}
diff --git a/requirements.txt b/requirements.txt
index 1861eeaa..58fe21b6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,11 +18,12 @@ gntp==1.0.3
html5lib==1.1
httpagentparser==1.9.2
idna==3.3
+importlib-metadata==4.11.3
importlib-resources==5.6.0
git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois
IPy==1.01
-Mako==1.1.6
-MarkupSafe==2.0.1
+Mako==1.2.0
+MarkupSafe==2.1.1
musicbrainzngs==0.7.1
packaging==21.3
paho-mqtt==1.6.1