#
# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# Copyright (C) 2018-2021 OpenCyphal Development Team <opencyphal.org>
# This software is distributed under the terms of the MIT License.
#
"""
Filters for generating python. All filters in this
module will be available in the template's global namespace as ``py``.
"""
from __future__ import annotations
import builtins
import functools
import keyword
import base64
import gzip
import pickle
import itertools
from typing import Any, Iterable
import pydsdl
from nunavut._dependencies import Dependencies
from nunavut._templates import (
SupportsTemplateContext,
template_context_filter,
template_language_filter,
template_language_int_filter,
template_language_list_filter,
)
from nunavut.lang import Language as BaseLanguage
from nunavut.lang._common import TokenEncoder, UniqueNameGenerator
class Language(BaseLanguage):
"""
Concrete, Python-specific :class:`nunavut.lang.Language` object.
"""
PYTHON_RESERVED_IDENTIFIERS: list[str] = sorted(list(map(str, list(keyword.kwlist) + dir(builtins))))
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._language_options["enable_serialization_asserts"] = True
@functools.lru_cache(maxsize=None)
def _get_token_encoder(self) -> TokenEncoder:
"""
Caching getter to ensure we don't have to recompile TokenEncoders for each filter invocation.
"""
return TokenEncoder(self, additional_reserved_identifiers=self.PYTHON_RESERVED_IDENTIFIERS)
def get_includes(self, dep_types: Dependencies) -> list[str]:
# imports aren't includes
return []
def filter_id(self, instance: Any, id_type: str = "any") -> str:
raw_name = self.default_filter_id_for_target(instance)
return self._get_token_encoder().strop(raw_name, id_type)
[docs]@template_context_filter
def filter_to_template_unique_name(context: SupportsTemplateContext, base_token: str) -> str:
"""
Filter that takes a base token and forms a name that is very
likely to be unique within the template the filter is invoked. This
name is also very likely to be a valid Python identifier.
.. IMPORTANT::
The exact tokens generated may change between major or minor versions
of this library. The only guarantee provided is that the tokens
will be stable for the same version of this library given the same
input.
Also note that name uniqueness is only likely within a given template.
Between templates there is no guarantee of uniqueness and,
since this library does not lex generated source, there is no guarantee
that the generated name does not conflict with a name generated by
another means.
.. invisible-code-block: python
from nunavut.lang.py import filter_to_template_unique_name
from nunavut.lang._common import UniqueNameGenerator
.. code-block:: python
# Given
template = '{{ "f" | to_template_unique_name }},{{ "f" | to_template_unique_name }},'
template += '{{ "f" | to_template_unique_name }},{{ "bar" | to_template_unique_name }}'
# then
rendered = '_f0_,_f1_,_f2_,_bar0_'
.. invisible-code-block: python
UniqueNameGenerator.reset()
jinja_filter_tester(filter_to_template_unique_name, template, rendered, 'py')
.. code-block:: python
# Given
template = '{{ "i like coffee" | to_template_unique_name }}'
# then
rendered = '_i like coffee0_'
.. invisible-code-block: python
jinja_filter_tester(filter_to_template_unique_name, template, rendered, 'py')
:param str base_token: A token to include in the base name.
:return: A name that is likely to be valid python identifier and is likely to
be unique within the file generated by the current template.
"""
return UniqueNameGenerator.get_instance()("py", base_token, "_", "_")
[docs]@template_language_filter(__name__)
def filter_id(language: Language, instance: Any, id_type: str = "any") -> str:
"""
Filter that produces a valid Python identifier for a given object. The encoding may not
be reversible.
.. invisible-code-block: python
from nunavut.lang.py import filter_id
.. code-block:: python
# Given
I = 'I like python'
# and
template = '{{ I | id }}'
# then
rendered = 'I_like_python'
.. invisible-code-block: python
jinja_filter_tester(filter_id, template, rendered, 'py', I=I)
.. code-block:: python
# Given
I = '&because'
# and
template = '{{ I | id }}'
# then
rendered = 'zX0026because'
.. invisible-code-block: python
jinja_filter_tester(filter_id, template, rendered, 'py', I=I)
.. code-block:: python
# Given
I = 'if'
# and
template = '{{ I | id }}'
# then
rendered = 'if_'
.. invisible-code-block: python
jinja_filter_tester(filter_id, template, rendered, 'py', I=I)
:param any instance: Any object or data that either has a name property or can be converted
to a string.
:return: A token that is a valid Python identifier, is not a reserved keyword, and is transformed
in a deterministic manner based on the provided instance.
"""
return language.filter_id(instance, id_type)
[docs]@template_language_filter(__name__)
def filter_full_reference_name(language: Language, t: pydsdl.CompositeType) -> str:
"""
Provides a string that is the full namespace, typename, major, and minor version for a given composite type.
.. invisible-code-block: python
from nunavut.lang.py import filter_full_reference_name
dummy = lambda: None
dummy_version = lambda: None
setattr(dummy, 'version', dummy_version)
.. code-block:: python
# Given
full_name = 'any.str.2Foo'
major = 1
minor = 2
# and
template = '{{ my_obj | full_reference_name }}'
# then
rendered = 'any_.str_.zX0032Foo_1_2'
.. invisible-code-block: python
setattr(dummy_version, 'major', major)
setattr(dummy_version, 'minor', minor)
setattr(dummy, 'full_name', full_name)
setattr(dummy, 'short_name', full_name.split('.')[-1])
jinja_filter_tester(filter_full_reference_name, template, rendered, 'py', my_obj=dummy)
:param pydsdl.CompositeType t: The DSDL type to get the fully-resolved reference name for.
"""
ns_parts = t.full_name.split(".")
if len(ns_parts) > 1:
if language.enable_stropping:
ns = list(map(functools.partial(filter_id, language), ns_parts[:-1]))
else:
ns = ns_parts[:-1]
return ".".join(ns + [language.filter_short_reference_name(t)])
[docs]@template_language_filter(__name__)
def filter_short_reference_name(language: Language, t: pydsdl.CompositeType) -> str:
"""
Provides a string that is a shorted version of the full reference name. This type is unique only within its
namespace.
.. invisible-code-block: python
from nunavut.lang.py import filter_short_reference_name
dummy = lambda: None
dummy_version = lambda: None
setattr(dummy, 'version', dummy_version)
.. code-block:: python
# Given
short_name = '2Foo'
major = 1
minor = 2
# and
template = '{{ my_obj | short_reference_name }}'
# then
rendered = 'zX0032Foo_1_2'
.. invisible-code-block: python
setattr(dummy_version, 'major', major)
setattr(dummy_version, 'minor', minor)
setattr(dummy, 'short_name', short_name)
jinja_filter_tester(filter_short_reference_name, template, rendered, 'py', my_obj=dummy)
:param pydsdl.CompositeType t: The DSDL type to get the reference name for.
"""
return language.filter_short_reference_name(t)
[docs]@template_language_list_filter(__name__)
def filter_imports(language: Language, t: pydsdl.CompositeType, sort: bool = True) -> list[str]:
"""
Returns a list of all modules that must be imported to use a given type.
:param pydsdl.CompositeType t: The type to scan for dependencies.
:param bool sort: If true the returned list will be sorted.
:return: a list of python module names the provided type depends on.
"""
# Make a list of all attributes defined by this type
if isinstance(t, pydsdl.ServiceType):
atr = t.request_type.attributes + t.response_type.attributes
else:
atr = t.attributes
def array_w_composite_type(data_type: pydsdl.Any) -> bool:
return isinstance(data_type, pydsdl.ArrayType) and isinstance(data_type.element_type, pydsdl.CompositeType)
# Extract data types of said attributes; for type constructors such as arrays extract the element type
dep_types = [x.data_type for x in atr if isinstance(x.data_type, pydsdl.CompositeType)]
dep_types += [x.data_type.element_type for x in atr if array_w_composite_type(x.data_type)]
# Make a list of unique full namespaces of referenced composites. Keep the original ordering.
namespace_list = []
for dt in dep_types:
ns = dt.full_namespace
if ns not in namespace_list:
namespace_list.append(ns)
if language.enable_stropping:
namespace_list = [".".join([language.filter_id(y) for y in x.split(".")]) for x in namespace_list]
if sort:
return list(sorted(namespace_list))
else:
return namespace_list
[docs]@template_language_int_filter(__name__)
def filter_longest_id_length(language: Language, attributes: list[pydsdl.Attribute]) -> int:
"""
Return the length of the longest identifier in a list of :class:`pydsdl.Attribute` objects.
.. invisible-code-block: python
from nunavut.lang.py import filter_longest_id_length
.. code-block:: python
# Given
I = ['one.str.int.any', 'three.str.int.any']
# and
template = '{{ I | longest_id_length }}'
# then
rendered = '32'
.. invisible-code-block: python
jinja_filter_tester(filter_longest_id_length, template, rendered, 'py', I=I)
"""
if language.enable_stropping:
return max(map(len, map(functools.partial(filter_id, language), attributes)))
else:
return max(map(len, attributes))
def filter_pickle(x: Any) -> str:
"""
Serializes the given object using pickle and then compresses it using gzip and then encodes it using base85.
"""
pck = base64.b85encode(gzip.compress(pickle.dumps(x, protocol=4))).decode().strip() # type: str
segment_gen = map("".join, itertools.zip_longest(*([iter(pck)] * 100), fillvalue=""))
return "\n".join(repr(x) for x in segment_gen)
def filter_numpy_scalar_type(t: pydsdl.Any) -> str:
"""
Returns the numpy scalar type that is the closest match to the given DSDL type.
"""
def pick_width(w: int) -> int:
for o in [8, 16, 32, 64]:
if w <= o:
return o
raise ValueError(f"Invalid bit width: {w}") # pragma: no cover
if isinstance(t, pydsdl.BooleanType):
return "_np_.bool_"
if isinstance(t, pydsdl.SignedIntegerType):
return f"_np_.int{pick_width(t.bit_length)}"
if isinstance(t, pydsdl.UnsignedIntegerType):
return f"_np_.uint{pick_width(t.bit_length)}"
if isinstance(t, pydsdl.FloatType):
return f"_np_.float{pick_width(t.bit_length)}"
assert not isinstance(t, pydsdl.PrimitiveType), "Forgot to handle some primitive types"
return "_np_.object_"
def filter_newest_minor_version_aliases(tys: Iterable[pydsdl.CompositeType]) -> list[tuple[str, pydsdl.CompositeType]]:
"""
Implementation of https://github.com/OpenCyphal/nunavut/issues/193
"""
tys = list(tys)
return [
(
f"{name}_{major}",
max(
(t for t in tys if t.short_name == name and t.version.major == major),
key=lambda x: int(x.version.minor),
),
)
for name, major in sorted({(x.short_name, x.version.major) for x in tys})
]