#
# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# Copyright (C) 2021 OpenCyphal Development Team <opencyphal.org>
# This software is distributed under the terms of the MIT License.
#
"""
A small collection of common utilities.
.. note::
Please don't use this as a dumping ground for things that belong in a dedicated package. Python being such a
full-featured language, there should be very few truly generic utilities in Nunavut.
"""
import collections.abc
import copy
import enum
import logging
import pathlib
from typing import Generator, MutableMapping, cast, TypeVar
import importlib_resources
_logger = logging.getLogger(__name__)
TEMPLATE_SUFFIX = ".j2" #: The suffix expected for nunavut templates.
[docs]@enum.unique
class YesNoDefault(enum.Enum):
"""
Trinary type for decisions that allow a default behavior to be requested that can
be different based on other contexts. For example:
.. invisible-code-block: python
from datetime import datetime
from nunavut._utilities import YesNoDefault
.. code-block:: python
def should_we_order_pizza(answer: YesNoDefault) -> bool:
if answer == YesNoDefault.YES or (
answer == YesNoDefault.DEFAULT and
datetime.today().isoweekday() == 5):
# if yes or if we are taking the default action which is to
# order pizza on Friday, and today is Friday, then we order pizza
return True
else:
return False
.. invisible-code-block: python
assert should_we_order_pizza(YesNoDefault.YES)
assert not should_we_order_pizza(YesNoDefault.NO)
"""
[docs] @classmethod
def test_truth(cls, ynd_value: "YesNoDefault", default_value: bool) -> bool:
"""
Helper method to test a YesNoDefault value and return a default boolean value.
.. invisible-code-block: python
from nunavut._utilities import YesNoDefault
.. code-block:: python
'''
let "is YES" be Y
let "is DEFAULT" be D where:
if Y then not D and if D then not Y
and "is NO" is Y = D = 0
let "is default_value true" be d
Y | D | d | Y or (D and d)
1 * * 1
0 1 0 0
0 1 1 1
0 0 * 0
'''
assert YesNoDefault.test_truth(YesNoDefault.YES, False)
assert not YesNoDefault.test_truth(YesNoDefault.DEFAULT, False)
assert YesNoDefault.test_truth(YesNoDefault.DEFAULT, True)
assert not YesNoDefault.test_truth(YesNoDefault.NO, True)
"""
if ynd_value == cls.DEFAULT:
return default_value
else:
return ynd_value == cls.YES
NO = 0
YES = 1
DEFAULT = 2
@enum.unique
class ResourceType(enum.Enum):
"""
Common Nunavut classifications for Python package resources.
"""
ANY = 0
CONFIGURATION = 1
SERIALIZATION_SUPPORT = 2
TYPE_SUPPORT = 3
@enum.unique
class ResourceSearchPolicy(enum.Enum):
"""
Generic policy type for controlling the behaviour of things that seach for resources.
"""
FIND_ALL = 0
FIND_FIRST = 1
def iter_package_resources(pkg_name: str, *suffix_filters: str) -> Generator[pathlib.Path, None, None]:
"""
>>> from nunavut._utilities import iter_package_resources
>>> rs = [x for x in iter_package_resources("nunavut.lang", ".py") if x.name == "__init__.py"]
>>> len(rs)
1
>>> rs[0].name
'__init__.py'
"""
for resource in importlib_resources.files(pkg_name).iterdir():
if resource.is_file() and isinstance(resource, pathlib.Path):
# Not sure why this works but it's seemed to so far. importlib_resources.as_file(resource)
# may be more correct but this can create temporary files which would disappear after the iterator
# had copied their paths. If you are reading this because this method isn't working for some packaging
# scheme then we may need to use importlib_resources.as_file(resource) to create a runtime cache of
# temporary objects that live for a given nunavut session. This, of course, wouldn't help across sessions
# which is a common use case when integrating Nunavut with build systems. So...here be dragons.
file_resource = resource
if any(suffix == file_resource.suffix for suffix in suffix_filters):
yield file_resource
def empty_list_support_files() -> Generator[pathlib.Path, None, None]:
"""
Helper for implementing the list_support_files method in language support packages. This provides an empty
iterator with the correct type annotations.
"""
# works in Python 3.3 and newer. Thanks https://stackoverflow.com/a/13243870
yield from ()
DeepUpdateType = TypeVar("DeepUpdateType", bound=MutableMapping)
def deep_update(target: DeepUpdateType, source: DeepUpdateType) -> DeepUpdateType:
"""
Helper method to do a recursive update of a map that may contain maps as values.
.. invisible-code-block: python
from nunavut._utilities import deep_update
import collections.abc
.. code-block:: python
target_map = {
"a": { "one": 1, "two": 2 },
"b": "not a map"
}
update_from = {
"a": { "two": { "i": "this value" }, "three": "that value"},
"c": "see"
}
target_map = deep_update(target_map, update_from)
update_from["a"]["two"]["i"] = "whoops, this was supposed to be a copy"
assert target_map["a"]["one"] == 1
assert isinstance(target_map["a"]["two"], collections.abc.Mapping)
assert target_map["a"]["two"]["i"] == "this value"
assert target_map["a"]["three"] == "that value"
assert target_map["b"] == "not a map"
assert "c" in target_map
assert target_map["c"] == "see"
"""
if isinstance(target, collections.abc.Mapping):
for key, value in source.items():
if isinstance(value, collections.abc.Mapping):
target[key] = deep_update(target.get(key, {}), cast(DeepUpdateType, value))
else:
target[key] = value
else:
target = copy.copy(source)
return target