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"(.*?)(?=\)", 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"(.*?)(?=\)", 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"\") if match: @@ -348,13 +325,13 @@ class Lexer(object): raise exceptions.SyntaxException( "Closing tag without opening tag: " % match.group(1), - **self.exception_kwargs + **self.exception_kwargs, ) elif self.tag[-1].keyword != match.group(1): raise exceptions.SyntaxException( "Closing tag 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>(.*?)", 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('&lt;User 1&gt;')\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