diff options
Diffstat (limited to 'lib/ansible')
-rwxr-xr-x | lib/ansible/cli/pull.py | 7 | ||||
-rw-r--r-- | lib/ansible/module_utils/ansible_release.py | 2 | ||||
-rw-r--r-- | lib/ansible/module_utils/common/json.py | 4 | ||||
-rw-r--r-- | lib/ansible/modules/pip.py | 2 | ||||
-rw-r--r-- | lib/ansible/parsing/yaml/dumper.py | 6 | ||||
-rw-r--r-- | lib/ansible/playbook/conditional.py | 9 | ||||
-rw-r--r-- | lib/ansible/playbook/task.py | 24 | ||||
-rw-r--r-- | lib/ansible/plugins/action/assert.py | 23 | ||||
-rw-r--r-- | lib/ansible/plugins/callback/__init__.py | 4 | ||||
-rw-r--r-- | lib/ansible/plugins/filter/core.py | 5 | ||||
-rw-r--r-- | lib/ansible/plugins/lookup/first_found.py | 15 | ||||
-rw-r--r-- | lib/ansible/release.py | 2 | ||||
-rw-r--r-- | lib/ansible/template/__init__.py | 15 | ||||
-rw-r--r-- | lib/ansible/utils/unsafe_proxy.py | 265 |
14 files changed, 354 insertions, 29 deletions
diff --git a/lib/ansible/cli/pull.py b/lib/ansible/cli/pull.py index dc8f055b..47084989 100755 --- a/lib/ansible/cli/pull.py +++ b/lib/ansible/cli/pull.py @@ -29,6 +29,7 @@ from ansible.plugins.loader import module_loader from ansible.utils.cmd_functions import run_cmd from ansible.utils.display import Display + display = Display() @@ -102,8 +103,8 @@ class PullCLI(CLI): 'This is a useful way to disperse git requests') self.parser.add_argument('-f', '--force', dest='force', default=False, action='store_true', help='run the playbook even if the repository could not be updated') - self.parser.add_argument('-d', '--directory', dest='dest', default=None, - help='absolute path of repository checkout directory (relative paths are not supported)') + self.parser.add_argument('-d', '--directory', dest='dest', default=None, type=opt_help.unfrack_path(), + help='path to the directory to which Ansible will checkout the repository.') self.parser.add_argument('-U', '--url', dest='url', default=None, help='URL of the playbook repository') self.parser.add_argument('--full', dest='fullclone', action='store_true', help='Do a full clone, instead of a shallow one.') self.parser.add_argument('-C', '--checkout', dest='checkout', @@ -134,7 +135,6 @@ class PullCLI(CLI): hostname = socket.getfqdn() # use a hostname dependent directory, in case of $HOME on nfs options.dest = os.path.join(C.ANSIBLE_HOME, 'pull', hostname) - options.dest = os.path.expandvars(os.path.expanduser(options.dest)) if os.path.exists(options.dest) and not os.path.isdir(options.dest): raise AnsibleOptionsError("%s is not a valid or accessible directory." % options.dest) @@ -307,6 +307,7 @@ class PullCLI(CLI): if context.CLIARGS['purge']: os.chdir('/') try: + display.debug("removing: %s" % context.CLIARGS['dest']) shutil.rmtree(context.CLIARGS['dest']) except Exception as e: display.error(u"Failed to remove %s: %s" % (context.CLIARGS['dest'], to_text(e))) diff --git a/lib/ansible/module_utils/ansible_release.py b/lib/ansible/module_utils/ansible_release.py index 6f0f794f..5fc1bde1 100644 --- a/lib/ansible/module_utils/ansible_release.py +++ b/lib/ansible/module_utils/ansible_release.py @@ -19,6 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -__version__ = '2.14.11' +__version__ = '2.14.13' __author__ = 'Ansible, Inc.' __codename__ = "C'mon Everybody" diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py index 727083ca..c4333fc1 100644 --- a/lib/ansible/module_utils/common/json.py +++ b/lib/ansible/module_utils/common/json.py @@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value): Used in ``AnsibleJSONEncoder.iterencode`` """ if _is_unsafe(value): - value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif is_sequence(value): value = [_preprocess_unsafe_encode(v) for v in value] elif isinstance(value, Mapping): @@ -63,7 +63,7 @@ class AnsibleJSONEncoder(json.JSONEncoder): value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')} elif getattr(o, '__UNSAFE__', False): # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode`` - value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif isinstance(o, Mapping): # hostvars and other objects value = dict(o) diff --git a/lib/ansible/modules/pip.py b/lib/ansible/modules/pip.py index a9930ccd..95a5d0d3 100644 --- a/lib/ansible/modules/pip.py +++ b/lib/ansible/modules/pip.py @@ -121,6 +121,8 @@ attributes: platform: platforms: posix notes: + - Python installations marked externally-managed (as defined by PEP668) cannot be updated by pip versions >= 23.0.1 without the use of + a virtual environment or setting the environment variable ``PIP_BREAK_SYSTEM_PACKAGES=1``. - The virtualenv (U(http://www.virtualenv.org/)) must be installed on the remote host if the virtualenv parameter is specified and the virtualenv needs to be created. diff --git a/lib/ansible/parsing/yaml/dumper.py b/lib/ansible/parsing/yaml/dumper.py index 8701bb81..bf2c0843 100644 --- a/lib/ansible/parsing/yaml/dumper.py +++ b/lib/ansible/parsing/yaml/dumper.py @@ -24,7 +24,7 @@ import yaml from ansible.module_utils.six import text_type, binary_type from ansible.module_utils.common.yaml import SafeDumper from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe from ansible.template import AnsibleUndefined from ansible.vars.hostvars import HostVars, HostVarsVars from ansible.vars.manager import VarsWithSources @@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data): def represent_unicode(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_str(self, text_type(data)) def represent_binary(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data)) diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py index fe07358c..d994f8f4 100644 --- a/lib/ansible/playbook/conditional.py +++ b/lib/ansible/playbook/conditional.py @@ -26,7 +26,7 @@ from jinja2.compiler import generate from jinja2.exceptions import UndefinedError from ansible import constants as C -from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError from ansible.module_utils.six import text_type from ansible.module_utils._text import to_native, to_text from ansible.playbook.attribute import FieldAttribute @@ -138,9 +138,10 @@ class Conditional: if not isinstance(conditional, text_type) or conditional == "": return conditional - # update the lookups flag, as the string returned above may now be unsafe - # and we don't want future templating calls to do unsafe things - disable_lookups |= hasattr(conditional, '__UNSAFE__') + # If the result of the first-pass template render (to resolve inline templates) is marked unsafe, + # explicitly fail since the next templating operation would never evaluate + if hasattr(conditional, '__UNSAFE__'): + raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.') # First, we do some low-level jinja2 parsing involving the AST format of the # statement to ensure we don't do anything unsafe (using the disable_lookup flag above) diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index ba35fcf0..a1a1162b 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -290,6 +290,30 @@ class Task(Base, Conditional, Taggable, CollectionSearch): super(Task, self).post_validate(templar) + def _post_validate_args(self, attr, value, templar): + # smuggle an untemplated copy of the task args for actions that need more control over the templating of their + # input (eg, debug's var/msg, assert's "that" conditional expressions) + self.untemplated_args = value + + # now recursively template the args dict + args = templar.template(value) + + # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something? + if '_variable_params' in args: + variable_params = args.pop('_variable_params') + if isinstance(variable_params, dict): + if C.INJECT_FACTS_AS_VARS: + display.warning("Using a variable for a task's 'args' is unsafe in some situations " + "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)") + variable_params.update(args) + args = variable_params + else: + # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up + # see https://github.com/ansible/ansible/issues/79862 + raise AnsibleError(f"invalid or malformed argument: '{variable_params}'") + + return args + def _post_validate_loop(self, attr, value, templar): ''' Override post validation for the loop field, which is templated diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py index 7721a6b4..e8ab6a9a 100644 --- a/lib/ansible/plugins/action/assert.py +++ b/lib/ansible/plugins/action/assert.py @@ -63,8 +63,29 @@ class ActionModule(ActionBase): quiet = boolean(self._task.args.get('quiet', False), strict=False) + # directly access 'that' via untemplated args from the task so we can intelligently trust embedded + # templates and preserve the original inputs/locations for better messaging on assert failures and + # errors. + # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value + # is not really an embedded expression) + # we could fix that by doing direct var lookups on the inputs + # FIXME: some form of this code should probably be shared between debug, assert, and + # Task.post_validate, since they + # have a lot of overlapping needs + try: + thats = self._task.untemplated_args['that'] + except KeyError: + # in the case of "we got our entire args dict from a template", we can just consult the + # post-templated dict (the damage has likely already been done for embedded templates anyway) + thats = self._task.args['that'] + + # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers + # (and even then, the leaf-most expression being wrapped is at least suboptimal + # (since its expression will be "eaten"). + if isinstance(thats, str): + thats = self._templar.template(thats) + # make sure the 'that' items are a list - thats = self._task.args['that'] if not isinstance(thats, list): thats = [thats] diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index d4fc347d..7646d293 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -38,7 +38,7 @@ from ansible.parsing.yaml.objects import AnsibleUnicode from ansible.plugins import AnsiblePlugin from ansible.utils.color import stringc from ansible.utils.display import Display -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe from ansible.vars.clean import strip_internal_keys, module_response_deepcopy import yaml @@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar): def _pretty_represent_str(self, data): """Uses block style for multi-line strings""" + if _is_unsafe(data): + data = data._strip_unsafe() data = text_type(data) if _should_use_block(data): style = '|' diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py index 52a2cd10..b7e2c11e 100644 --- a/lib/ansible/plugins/filter/core.py +++ b/lib/ansible/plugins/filter/core.py @@ -37,6 +37,7 @@ from ansible.utils.display import Display from ansible.utils.encrypt import passlib_or_crypt from ansible.utils.hashing import md5s, checksum_s from ansible.utils.unicode import unicode_wrap +from ansible.utils.unsafe_proxy import _is_unsafe from ansible.utils.vars import merge_hash display = Display() @@ -215,6 +216,8 @@ def from_yaml(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load(text_type(to_text(data, errors='surrogate_or_strict'))) return data @@ -224,6 +227,8 @@ def from_yaml_all(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict'))) return data diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py index 5b94b103..a882db01 100644 --- a/lib/ansible/plugins/lookup/first_found.py +++ b/lib/ansible/plugins/lookup/first_found.py @@ -136,7 +136,6 @@ RETURN = """ elements: path """ import os -import re from collections.abc import Mapping, Sequence @@ -147,10 +146,22 @@ from ansible.module_utils.six import string_types from ansible.plugins.lookup import LookupBase +def _splitter(value, chars): + chars = set(chars) + v = '' + for c in value: + if c in chars: + yield v + v = '' + continue + v += c + yield v + + def _split_on(terms, spliters=','): termlist = [] if isinstance(terms, string_types): - termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms) + termlist = list(_splitter(terms, spliters)) else: # added since options will already listify for t in terms: diff --git a/lib/ansible/release.py b/lib/ansible/release.py index 6f0f794f..5fc1bde1 100644 --- a/lib/ansible/release.py +++ b/lib/ansible/release.py @@ -19,6 +19,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -__version__ = '2.14.11' +__version__ = '2.14.13' __author__ = 'Ansible, Inc.' __codename__ = "C'mon Everybody" diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index baa85ed7..c45cfe35 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -31,7 +31,7 @@ from contextlib import contextmanager from numbers import Number from traceback import format_exc -from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError from jinja2.loaders import FileSystemLoader from jinja2.nativetypes import NativeEnvironment from jinja2.runtime import Context, StrictUndefined @@ -55,7 +55,7 @@ from ansible.template.vars import AnsibleJ2Vars from ansible.utils.display import Display from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.native_jinja import NativeJinjaText -from ansible.utils.unsafe_proxy import wrap_var +from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText display = Display() @@ -332,10 +332,21 @@ class AnsibleContext(Context): flag is checked post-templating, and (when set) will result in the final templated result being wrapped in AnsibleUnsafe. ''' + _disallowed_callables = frozenset({ + AnsibleUnsafeText._strip_unsafe.__qualname__, + AnsibleUnsafeBytes._strip_unsafe.__qualname__, + NativeJinjaUnsafeText._strip_unsafe.__qualname__, + }) + def __init__(self, *args, **kwargs): super(AnsibleContext, self).__init__(*args, **kwargs) self.unsafe = False + def call(self, obj, *args, **kwargs): + if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables: + raise SecurityError(f"{obj!r} is not safely callable") + return super().call(obj, *args, **kwargs) + def _is_unsafe(self, val): ''' Our helper function, which will also recursively check dict and diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py index d78ebf6e..683f6e27 100644 --- a/lib/ansible/utils/unsafe_proxy.py +++ b/lib/ansible/utils/unsafe_proxy.py @@ -57,7 +57,6 @@ from collections.abc import Mapping, Set from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.common.collections import is_sequence -from ansible.module_utils.six import string_types, binary_type, text_type from ansible.utils.native_jinja import NativeJinjaText @@ -68,16 +67,256 @@ class AnsibleUnsafe(object): __UNSAFE__ = True -class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe): - def decode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs)) +class AnsibleUnsafeBytes(bytes, AnsibleUnsafe): + def _strip_unsafe(self): + return super().__bytes__() + def __reduce__(self, /): + return (self.__class__, (self._strip_unsafe(),)) -class AnsibleUnsafeText(text_type, AnsibleUnsafe): - def encode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs)) + def __str__(self, /): # pylint: disable=invalid-str-returned + return self.decode() + + def __bytes__(self, /): # pylint: disable=invalid-bytes-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return AnsibleUnsafeText(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return AnsibleUnsafeText(super().__format__(format_spec)) + + def __getitem__(self, key, /): + if isinstance(key, int): + return super().__getitem__(key) + return self.__class__(super().__getitem__(key)) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def center(self, width, fillchar=b' ', /): + return self.__class__(super().center(width, fillchar)) + + def decode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def join(self, iterable_of_bytes, /): + return self.__class__(super().join(iterable_of_bytes)) + + def ljust(self, width, fillchar=b' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, chars=None, /): + return self.__class__(super().lstrip(chars)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=b' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, chars=None, /): + return self.__class__(super().rstrip(chars)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, chars=None, /): + return self.__class__(super().strip(chars)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /, delete=b''): + return self.__class__(super().translate(table, delete=delete)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) + + +class AnsibleUnsafeText(str, AnsibleUnsafe): + def _strip_unsafe(self, /): + return super().__str__() + + def __reduce__(self, /): + return (self.__class__, (self._strip_unsafe(),)) + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return self.__class__(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=' ', /): + return self.__class__(super().center(width, fillchar)) + + def encode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable, /): + return self.__class__(super().join(iterable)) + + def ljust(self, width, fillchar=' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, chars=None, /): + return self.__class__(super().lstrip(chars)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, chars=None, /): + return self.__class__(super().rstrip(chars)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, chars=None, /): + return self.__class__(super().strip(chars)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /): + return self.__class__(super().translate(table)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText): @@ -112,9 +351,9 @@ def wrap_var(v): v = _wrap_sequence(v) elif isinstance(v, NativeJinjaText): v = NativeJinjaUnsafeText(v) - elif isinstance(v, binary_type): + elif isinstance(v, bytes): v = AnsibleUnsafeBytes(v) - elif isinstance(v, text_type): + elif isinstance(v, str): v = AnsibleUnsafeText(v) return v @@ -126,3 +365,7 @@ def to_unsafe_bytes(*args, **kwargs): def to_unsafe_text(*args, **kwargs): return wrap_var(to_text(*args, **kwargs)) + + +def _is_unsafe(obj): + return getattr(obj, '__UNSAFE__', False) is True |