Improved efro.dataclasses functionality

This commit is contained in:
Eric Froemling 2021-01-14 23:00:42 -08:00
parent efe7050445
commit 4aa178e337
14 changed files with 538 additions and 266 deletions

View File

@ -3936,14 +3936,14 @@
"build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/82/81/1ae81275ebe9ab9ab502b7eebb32",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/10/cf/d799dd2e5e833a42330a91a27872",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f3/66/4b663b0bd3d346168132a3b76e3d",
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/26/e42cb423191515afb14ed85da586",
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/90/5d/b4f96e9882d98e83a7ba33c9db22",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/14/f7/5ad112c496608b9cb82bf33f8bb7",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/46/ca/13bc7ce2e07eeee54c3309c960b7",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/79/65/c7a4dcab9c1f5f0b17ded9d2a0ac",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/c1/151d5e57bf09762496470bf8c4bb",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0c/e1/2335a8137464182089e57ace0f8e",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/30/a7/af58cca4a647d19fca0a43b11d88",
"build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/21/e9/1430bf257d1263ff0ed36410c332",
"build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/46/7a/caed7137d70eb5bf1696a879fb75",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d5/e5/c6ebd43787923294b2210592290d",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/23/2b/74da96e45034770834c5213cfaf0",
"build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/11/f6/ff5475943c5c106266121392faab",
"build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/71/d5/009c34dd2016fa879c28c8838405",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/85/c8/19cf729b848e002fdc731decba21",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bc/7b/c93b714145994b823a708b57b501",
"build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e8/bb/5985b1670513fbcd4931383af8a0",
"build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/69/51/2fd61101ec41e2944db05fa588db",
"build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a6/9b/f3c55efe1ae7a8fd3ba35d3f9caa",
@ -3952,12 +3952,12 @@
"build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/83/25/980050d75bbea49a84652209050c",
"build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/47/aa/e82233695a50974e7e22db4e7146",
"build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2b/45/7f9fbae208890455fce2fbc172d3",
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/4c/5d82b9c31124da7a329358fcd553",
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ad/71/ed5d6a378aca34d43483aed9dc79",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/30/e5/284e85af992a18c1053adc58cdfb",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/23/ca/f4cfaabc60d0afa57b6c0f38547f",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0e/47/cf7099fc7b7317c2be02861e6978",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/74/ef/7f5408bb25f0ebcf17e52ad18442",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/04/42c2bf1a43ec9c2c674289f5b963",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a5/5d/06f1fc4de7443709de1acfc00960"
"build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4c/f2/67f1041d8c49e7bdad100dae4ef1",
"build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/80/7b/f57451f57d2434bcb75d48c08a33",
"build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c6/ce/1df69a933f207867f8385f491f47",
"build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/21/49/0b011f4ce5872c7eaabaa97b890f",
"build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/20/6f/2fc08ac6c2b2c3b31572a369deab",
"build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1d/bf/ce1ea8ad2fb7bcf58a367ec1e19a",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fb/0b/864980222e52ee6bd5a272b0f742",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bb/6d/401e3cb8d8641df64ffbeac2b02d"
}

View File

@ -174,12 +174,14 @@
<w>basew</w>
<w>bastd</w>
<w>batools</w>
<w>bbbb</w>
<w>bblk</w>
<w>bblu</w>
<w>bbot</w>
<w>bbtn</w>
<w>bcppcompiler</w>
<w>bcyn</w>
<w>bdea</w>
<w>bdfl</w>
<w>bdir</w>
<w>belarussian</w>
@ -314,6 +316,7 @@
<w>cheadersline</w>
<w>checkarg</w>
<w>checkboxwidget</w>
<w>checkchisel</w>
<w>checkenv</w>
<w>checkfast</w>
<w>checkfull</w>
@ -592,6 +595,7 @@
<w>efrocachemap</w>
<w>efroemling</w>
<w>efrogradle</w>
<w>efrohack</w>
<w>efrohome</w>
<w>efrosync</w>
<w>efrotool</w>
@ -623,6 +627,8 @@
<w>entrytypeselect</w>
<w>enumtype</w>
<w>enumval</w>
<w>enumvalue</w>
<w>enval</w>
<w>envcfg</w>
<w>envhash</w>
<w>envname</w>
@ -697,6 +703,8 @@
<w>fhashes</w>
<w>fhdr</w>
<w>fieldattr</w>
<w>fieldname</w>
<w>fieldpath</w>
<w>fieldsdict</w>
<w>fieldtype</w>
<w>fieldtypes</w>
@ -1021,6 +1029,7 @@
<w>inputfiles</w>
<w>inputhash</w>
<w>inputnode</w>
<w>inputter</w>
<w>inputtype</w>
<w>inpututils</w>
<w>inspectdir</w>
@ -1117,6 +1126,7 @@
<w>leaderboards</w>
<w>leady</w>
<w>lenglishvalues</w>
<w>lenval</w>
<w>levelgametype</w>
<w>levelmodule</w>
<w>levelname</w>
@ -1413,6 +1423,7 @@
<w>numedit</w>
<w>numsound</w>
<w>numstr</w>
<w>nval</w>
<w>nvcompress</w>
<w>nvidia</w>
<w>nyko</w>
@ -1424,6 +1435,7 @@
<w>obval</w>
<w>occurrances</w>
<w>oculus</w>
<w>oenval</w>
<w>offsanchor</w>
<w>ofval</w>
<w>oggenc</w>
@ -1465,6 +1477,8 @@
<w>outhashpath</w>
<w>outname</w>
<w>outpath</w>
<w>outputter</w>
<w>outvalue</w>
<w>ouya</w>
<w>overloadsigs</w>
<w>packagedir</w>
@ -1829,6 +1843,8 @@
<w>sdtk</w>
<w>selectmodule</w>
<w>senze</w>
<w>seqtype</w>
<w>seqtypestr</w>
<w>serverbuild</w>
<w>servercallthread</w>
<w>servercallthreadtype</w>
@ -1882,6 +1898,7 @@
<w>shroom</w>
<w>shutil</w>
<w>simplesubclasses</w>
<w>simpletype</w>
<w>sincelaunch</w>
<w>singledispatch</w>
<w>singledispatchmethod</w>
@ -1951,6 +1968,7 @@
<w>sred</w>
<w>sshd</w>
<w>sslproto</w>
<w>ssval</w>
<w>stackstr</w>
<w>standin</w>
<w>starscale</w>
@ -1995,6 +2013,7 @@
<w>subdep</w>
<w>subdeps</w>
<w>subdirs</w>
<w>subfieldpath</w>
<w>subfolders</w>
<w>subname</w>
<w>subpath</w>
@ -2005,6 +2024,7 @@
<w>subprocesses</w>
<w>subrepos</w>
<w>subsel</w>
<w>subtypestr</w>
<w>subval</w>
<w>subvalue</w>
<w>subvaluetype</w>
@ -2074,6 +2094,7 @@
<w>tempfile</w>
<w>tempfilepath</w>
<w>templatecb</w>
<w>tenum</w>
<w>termcolors</w>
<w>termios</w>
<w>testbuffer</w>

View File

@ -654,17 +654,17 @@ test-full: test
# Individual test with extra output enabled.
test-assetmanager:
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
tests/test_ba/test_assetmanager.py::test_assetmanager
# Individual test with extra output enabled.
test-dataclasses:
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
tests/test_efro/test_dataclasses.py
# Individual test with extra output enabled.
test-entity:
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -v \
@tools/pcommand pytest -o log_cli=true -o log_cli_level=debug -s -vv \
tests/test_efro/test_entity.py
# Tell make which of these targets don't represent files.

View File

@ -60,8 +60,7 @@ from ba._campaign import Campaign
from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
timestring, cameraflash)
from ba._general import (WeakCall, Call, existing, Existable,
verify_object_death, storagename, getclass,
enum_by_value)
verify_object_death, storagename, getclass)
from ba._keyboard import Keyboard
from ba._level import Level
from ba._lobby import Lobby, Chooser

View File

@ -8,7 +8,6 @@ import types
import weakref
import random
import inspect
from enum import Enum
from typing import TYPE_CHECKING, TypeVar, Protocol
from efro.terminal import Clr
@ -36,7 +35,6 @@ class Existable(Protocol):
ExistableType = TypeVar('ExistableType', bound=Existable)
T = TypeVar('T')
ET = TypeVar('ET', bound=Enum)
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
@ -399,30 +397,3 @@ def storagename(suffix: str = None) -> str:
if suffix is not None:
fullpath = f'{fullpath}_{suffix}'
return fullpath.replace('.', '_')
def enum_by_value(cls: Type[ET], value: Any) -> ET:
"""Create an enum from a value.
Category: General Utility Functions
This is basically the same as doing 'obj = EnumType(value)' except
that it works around an issue where a reference loop is created
if an exception is thrown due to an invalid value. Since we disable
the cyclic garbage collector for most of the time, such loops can lead
to our objects sticking around longer than we want.
This issue has been submitted to Python as a bug so hopefully we can
remove this eventually if it gets fixed: https://bugs.python.org/issue42248
"""
# Note: we don't recreate *ALL* the functionality of the Enum constructor
# such as the _missing_ hook; but this should cover our basic needs.
value2member_map = getattr(cls, '_value2member_map_')
assert value2member_map is not None
try:
out = value2member_map[value]
assert isinstance(out, cls)
return out
except KeyError:
raise ValueError('%r is not a valid %s' %
(value, cls.__name__)) from None

View File

@ -241,6 +241,7 @@ class GatherWindow(ba.Window):
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
from efro.util import enum_by_value
try:
for tab in self._tabs.values():
tab.restore_state()
@ -252,7 +253,7 @@ class GatherWindow(ba.Window):
current_tab = self.TabID.ABOUT
gather_tab_val = ba.app.config.get('Gather Tab')
try:
stored_tab = ba.enum_by_value(self.TabID, gather_tab_val)
stored_tab = enum_by_value(self.TabID, gather_tab_val)
if stored_tab in self._tab_row.tabs:
current_tab = stored_tab
except ValueError:
@ -264,8 +265,8 @@ class GatherWindow(ba.Window):
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
sel_tab_id = enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.ABOUT
sel = self._tab_row.tabs[sel_tab_id].button

View File

@ -1018,6 +1018,7 @@ class StoreBrowserWindow(ba.Window):
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
from efro.util import enum_by_value
try:
sel: Optional[ba.Widget]
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
@ -1025,8 +1026,8 @@ class StoreBrowserWindow(ba.Window):
assert isinstance(sel_name, (str, type(None)))
try:
current_tab = ba.enum_by_value(self.TabID,
ba.app.config.get('Store Tab'))
current_tab = enum_by_value(self.TabID,
ba.app.config.get('Store Tab'))
except ValueError:
current_tab = self.TabID.CHARACTERS
@ -1040,8 +1041,8 @@ class StoreBrowserWindow(ba.Window):
sel = self._scrollwidget
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
sel_tab_id = enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.CHARACTERS
sel = self._tab_row.tabs[sel_tab_id].button

View File

@ -503,14 +503,15 @@ class WatchWindow(ba.Window):
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
from efro.util import enum_by_value
try:
sel: Optional[ba.Widget]
sel_name = ba.app.ui.window_states.get(self.__class__.__name__,
{}).get('sel_name')
assert isinstance(sel_name, (str, type(None)))
try:
current_tab = ba.enum_by_value(self.TabID,
ba.app.config.get('Watch Tab'))
current_tab = enum_by_value(self.TabID,
ba.app.config.get('Watch Tab'))
except ValueError:
current_tab = self.TabID.MY_REPLAYS
self._set_tab(current_tab)
@ -521,8 +522,8 @@ class WatchWindow(ba.Window):
sel = self._tab_container
elif isinstance(sel_name, str) and sel_name.startswith('Tab:'):
try:
sel_tab_id = ba.enum_by_value(self.TabID,
sel_name.split(':')[-1])
sel_tab_id = enum_by_value(self.TabID,
sel_name.split(':')[-1])
except ValueError:
sel_tab_id = self.TabID.MY_REPLAYS
sel = self._tab_row.tabs[sel_tab_id].button

View File

@ -23,7 +23,7 @@ sys.path += [
]
from bacommon.servermanager import ServerConfig, StartServerModeCommand
from efro.dataclasses import dataclass_assign, dataclass_validate
from efro.dataclasses import dataclass_from_dict, dataclass_validate
from efro.error import CleanError
from efro.terminal import Clr
@ -32,9 +32,11 @@ if TYPE_CHECKING:
from types import FrameType
from bacommon.servermanager import ServerCommand
VERSION_STR = '1.1.0'
VERSION_STR = '1.1.1'
# Version history:
# 1.1.1:
# Switched config reading to use efro.dataclasses.dataclass_from_dict()
# 1.1.0:
# Added shutdown command
# Changed restart to default to immediate=True
@ -274,19 +276,17 @@ class ServerManagerApp:
def _load_config(self) -> ServerConfig:
user_config_path = 'config.yaml'
# Start with a default config, and if there is a config.yaml,
# assign whatever is contained within.
config = ServerConfig()
if os.path.exists(user_config_path):
import yaml
with open(user_config_path) as infile:
user_config = yaml.safe_load(infile.read())
user_config_raw = yaml.safe_load(infile.read())
# An empty config file will yield None, and that's ok.
if user_config is not None:
dataclass_assign(config, user_config)
if user_config_raw is not None:
return dataclass_from_dict(ServerConfig, user_config_raw)
return config
# Go with defaults if we weren't able to load anything.
return ServerConfig()
def _enable_tab_completion(self, locs: Dict) -> None:
"""Enable tab-completion on platforms where available (linux/mac)."""

View File

@ -70,6 +70,7 @@
<w>bbbbbb</w>
<w>bbbbbbb</w>
<w>bcfn</w>
<w>bdea</w>
<w>bezanson</w>
<w>bgra</w>
<w>bigendian</w>
@ -142,6 +143,7 @@
<w>charstr</w>
<w>chatmessage</w>
<w>checkboxwidget</w>
<w>checkchisel</w>
<w>chrono</w>
<w>chunksize</w>
<w>cjief</w>
@ -246,6 +248,7 @@
<w>echidna</w>
<w>edef</w>
<w>efro</w>
<w>efrohack</w>
<w>efrohome</w>
<w>elems</w>
<w>elevenbase</w>
@ -258,6 +261,8 @@
<w>endline</w>
<w>endtime</w>
<w>entrypoint</w>
<w>enumvalue</w>
<w>enval</w>
<w>envcfg</w>
<w>envs</w>
<w>envval</w>
@ -297,6 +302,8 @@
<w>fffffff</w>
<w>fffffffffifff</w>
<w>fgets</w>
<w>fieldname</w>
<w>fieldpath</w>
<w>fifteenbits</w>
<w>filterval</w>
<w>finishedptr</w>
@ -427,6 +434,7 @@
<w>initguid</w>
<w>inittab</w>
<w>inputdevice</w>
<w>inputter</w>
<w>insta</w>
<w>intercollide</w>
<w>internalformat</w>
@ -473,6 +481,7 @@
<w>lastvalid</w>
<w>leaderboard</w>
<w>leaderboards</w>
<w>lenval</w>
<w>lgui</w>
<w>lhalf</w>
<w>libutf</w>
@ -593,6 +602,7 @@
<w>numc</w>
<w>numentries</w>
<w>numlock</w>
<w>nval</w>
<w>nvidia</w>
<w>nyffenegger</w>
<w>objexists</w>
@ -600,6 +610,7 @@
<w>obstack</w>
<w>obvs</w>
<w>oculus</w>
<w>oenval</w>
<w>oiffsss</w>
<w>oldname</w>
<w>oooo</w>
@ -630,7 +641,9 @@
<w>ostype</w>
<w>ourself</w>
<w>ourstanding</w>
<w>outputter</w>
<w>outval</w>
<w>outvalue</w>
<w>ouya</w>
<w>parameteriv</w>
<w>passcode</w>
@ -752,6 +765,8 @@
<w>sdl's</w>
<w>sdlk</w>
<w>seqlen</w>
<w>seqtype</w>
<w>seqtypestr</w>
<w>serv</w>
<w>serverget</w>
<w>serverput</w>
@ -780,6 +795,7 @@
<w>shufflable</w>
<w>signsubscale</w>
<w>simd</w>
<w>simpletype</w>
<w>sisssssssss</w>
<w>sixteenbits</w>
<w>smoothering</w>
@ -810,6 +826,7 @@
<w>sssssssd</w>
<w>sssssssi</w>
<w>ssssssssssss</w>
<w>ssval</w>
<w>standin</w>
<w>startedptr</w>
<w>startpos</w>
@ -832,11 +849,13 @@
<w>subargs</w>
<w>subclsssing</w>
<w>subentities</w>
<w>subfieldpath</w>
<w>subitems</w>
<w>subpaths</w>
<w>subplatform</w>
<w>subscale</w>
<w>subscr</w>
<w>subtypestr</w>
<w>sval</w>
<w>symbolification</w>
<w>syscalls</w>
@ -849,6 +868,7 @@
<w>teleported</w>
<w>teleporting</w>
<w>tempvec</w>
<w>tenum</w>
<w>testint</w>
<w>testnode</w>
<w>texel</w>

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2021-01-08 for Ballistica version 1.5.30 build 20266</em></h4>
<h4><em>last updated on 2021-01-14 for Ballistica version 1.5.30 build 20267</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>
@ -86,7 +86,6 @@
<ul>
<li><a href="#function_ba_charstr">ba.charstr()</a></li>
<li><a href="#function_ba_do_once">ba.do_once()</a></li>
<li><a href="#function_ba_enum_by_value">ba.enum_by_value()</a></li>
<li><a href="#function_ba_garbage_collect">ba.garbage_collect()</a></li>
<li><a href="#function_ba_getclass">ba.getclass()</a></li>
<li><a href="#function_ba_is_browser_likely_available">ba.is_browser_likely_available()</a></li>
@ -6579,22 +6578,6 @@ the background and just looks pretty; it does not affect gameplay.
Note that the actual amount emitted may vary depending on graphics
settings, exiting element counts, or other factors.</p>
<hr>
<h2><strong><a name="function_ba_enum_by_value">ba.enum_by_value()</a></strong></h3>
<p><span>enum_by_value(cls: Type[ET], value: Any) -&gt; ET</span></p>
<p>Create an enum from a value.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>This is basically the same as doing 'obj = EnumType(value)' except
that it works around an issue where a reference loop is created
if an exception is thrown due to an invalid value. Since we disable
the cyclic garbage collector for most of the time, such loops can lead
to our objects sticking around longer than we want.
This issue has been submitted to Python as a bug so hopefully we can
remove this eventually if it gets fixed: https://bugs.python.org/issue42248</p>
<hr>
<h2><strong><a name="function_ba_existing">ba.existing()</a></strong></h3>
<p><span>existing(obj: Optional[ExistableType]) -&gt; Optional[ExistableType]</span></p>

View File

@ -4,19 +4,33 @@
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
import pytest
from efro.dataclasses import dataclass_assign, dataclass_validate
from efro.dataclasses import (dataclass_validate, dataclass_from_dict,
dataclass_to_dict)
if TYPE_CHECKING:
from typing import Optional, List
from typing import Optional, List, Set
class _EnumTest(Enum):
TEST1 = 'test1'
TEST2 = 'test2'
@dataclass
class _NestedClass:
ival: int = 0
sval: str = 'foo'
def test_assign() -> None:
"""Testing various assignments."""
# pylint: disable=too-many-statements
@dataclass
@ -25,120 +39,181 @@ def test_assign() -> None:
sval: str = ''
bval: bool = True
fval: float = 1.0
nval: _NestedClass = field(default_factory=_NestedClass)
enval: _EnumTest = _EnumTest.TEST1
oival: Optional[int] = None
osval: Optional[str] = None
obval: Optional[bool] = None
ofval: Optional[float] = None
oenval: Optional[_EnumTest] = _EnumTest.TEST1
lsval: List[str] = field(default_factory=list)
lival: List[int] = field(default_factory=list)
lbval: List[bool] = field(default_factory=list)
lfval: List[float] = field(default_factory=list)
tclass = _TestClass()
lenval: List[_EnumTest] = field(default_factory=list)
ssval: Set[str] = field(default_factory=set)
class _TestClass2:
pass
tclass2 = _TestClass2()
# Arg types:
# Attempting to use with non-dataclass should fail.
with pytest.raises(TypeError):
dataclass_assign(tclass2, {})
dataclass_from_dict(_TestClass2, {})
# Attempting to pass non-dicts should fail.
with pytest.raises(TypeError):
dataclass_assign(tclass, []) # type: ignore
dataclass_from_dict(_TestClass, []) # type: ignore
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, None) # type: ignore
# Invalid attrs.
# Passing an attr not in the dataclass should fail.
with pytest.raises(AttributeError):
dataclass_assign(tclass, {'nonexistent': 'foo'})
dataclass_from_dict(_TestClass, {'nonexistent': 'foo'})
# Correct types.
dataclass_assign(
tclass, {
# A dict containing *ALL* values should match what we
# get when creating a dataclass and then converting back
# to a dict.
dict1 = {
'ival': 1,
'sval': 'foo',
'bval': True,
'fval': 2.0,
'nval': {
'ival': 1,
'sval': 'foo',
'bval': True,
'fval': 2.0,
'lsval': ['foo'],
'lival': [10],
'lbval': [False],
'lfval': [1.0]
})
dataclass_assign(
tclass, {
'oival': None,
'osval': None,
'obval': None,
'ofval': None,
'lsval': [],
'lival': [],
'lbval': [],
'lfval': []
})
dataclass_assign(
tclass, {
'oival': 1,
'osval': 'foo',
'obval': True,
'ofval': 2.0,
'lsval': ['foo', 'bar', 'eep'],
'lival': [10, 11, 12],
'lbval': [False, True],
'lfval': [1.0, 2.0, 3.0]
})
'sval': 'bar'
},
'enval': 'test1',
'oival': 1,
'osval': 'foo',
'obval': True,
'ofval': 1.0,
'oenval': 'test2',
'lsval': ['foo'],
'lival': [10],
'lbval': [False],
'lfval': [1.0],
'lenval': ['test1', 'test2'],
'ssval': ['foo']
}
dc1 = dataclass_from_dict(_TestClass, dict1)
assert dataclass_to_dict(dc1) == dict1
# Type mismatches.
with pytest.raises(TypeError):
dataclass_assign(tclass, {'ival': 'foo'})
# A few other assignment checks.
assert isinstance(
dataclass_from_dict(
_TestClass, {
'oival': None,
'osval': None,
'obval': None,
'ofval': None,
'lsval': [],
'lival': [],
'lbval': [],
'lfval': [],
'ssval': []
}), _TestClass)
assert isinstance(
dataclass_from_dict(
_TestClass, {
'oival': 1,
'osval': 'foo',
'obval': True,
'ofval': 2.0,
'lsval': ['foo', 'bar', 'eep'],
'lival': [10, 11, 12],
'lbval': [False, True],
'lfval': [1.0, 2.0, 3.0]
}), _TestClass)
# Attr assigns mismatched with their value types should fail.
with pytest.raises(TypeError):
dataclass_assign(tclass, {'sval': 1})
dataclass_from_dict(_TestClass, {'ival': 'foo'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'sval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'bval': 2})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'oival': 'foo'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'osval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'obval': 2})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ofval': 'blah'})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'oenval': 'test3'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': 'blah'})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': ['blah', None]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': [1]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lsval': (1, )})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lbval': [None]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lival': ['foo']})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lfval': [True]})
with pytest.raises(ValueError):
dataclass_from_dict(_TestClass, {'lenval': ['test1', 'test3']})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': [True]})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': {}})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ssval': set()})
# More subtle attr/type mismatches that should fail
# (we currently require EXACT type matches).
with pytest.raises(TypeError):
dataclass_assign(tclass, {'bval': 2})
dataclass_from_dict(_TestClass, {'ival': True})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'fval': 2}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'bval': 1})
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'ofval': 1}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_from_dict(_TestClass, {'lfval': [1]}, coerce_to_float=False)
with pytest.raises(TypeError):
dataclass_assign(tclass, {'oival': 'foo'})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'osval': 1})
def test_coerce() -> None:
"""Test value coercion."""
with pytest.raises(TypeError):
dataclass_assign(tclass, {'obval': 2})
@dataclass
class _TestClass:
ival: int = 0
fval: float = 0.0
# Float value present for int should never work.
obj = _TestClass()
# noinspection PyTypeHints
obj.ival = 1.0 # type: ignore
with pytest.raises(TypeError):
dataclass_assign(tclass, {'ofval': 'blah'})
dataclass_validate(obj, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_validate(obj, coerce_to_float=False)
# Int value present for float should work only with coerce on.
obj = _TestClass()
obj.fval = 1
dataclass_validate(obj, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lsval': 'blah'})
dataclass_validate(obj, coerce_to_float=False)
# Likewise, passing in an int for a float field should work only
# with coerce on.
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lsval': [1]})
dataclass_from_dict(_TestClass, {'fval': 1}, coerce_to_float=False)
# Passing in floats for an int field should never work.
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lbval': [None]})
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=True)
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lival': ['foo']})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lfval': [True]})
# More subtle ones (we currently require EXACT type matches)
with pytest.raises(TypeError):
dataclass_assign(tclass, {'ival': True})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'fval': 2})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'bval': 1})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'ofval': 1})
with pytest.raises(TypeError):
dataclass_assign(tclass, {'lfval': [1]})
dataclass_from_dict(_TestClass, {'ival': 1.0}, coerce_to_float=False)
def test_validate() -> None:
@ -159,10 +234,10 @@ def test_validate() -> None:
tclass = _TestClass()
dataclass_validate(tclass)
# No longer valid.
# No longer valid (without coerce)
tclass.fval = 1
with pytest.raises(TypeError):
dataclass_validate(tclass)
dataclass_validate(tclass, coerce_to_float=False)
# Should pass by default.
tclass = _TestClass()

View File

@ -1,122 +1,295 @@
# Released under the MIT License. See LICENSE for details.
#
"""Custom functionality for dealing with dataclasses."""
# Note: We do lots of comparing of exact types here which is normally
# frowned upon (stuff like isinstance() is usually encouraged).
# pylint: disable=unidiomatic-typecheck
from __future__ import annotations
import dataclasses
from typing import TYPE_CHECKING
import inspect
from enum import Enum
from typing import TYPE_CHECKING, TypeVar, Generic
from efro.util import enum_by_value
if TYPE_CHECKING:
from typing import Any, Dict, Type, Tuple
from typing import Any, Dict, Type, Tuple, Optional
# For fields with these string types, we require a passed value's type
# to exactly match one of the tuple values to consider the assignment valid.
_SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = {
'int': (int, ),
'str': (str, ),
'bool': (bool, ),
'float': (float, ),
'Optional[int]': (int, type(None)),
'Optional[str]': (str, type(None)),
'Optional[bool]': (bool, type(None)),
'Optional[float]': (float, type(None)),
}
_LIST_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = {
'List[int]': (int, ),
'List[str]': (str, ),
'List[bool]': (bool, ),
'List[float]': (float, ),
T = TypeVar('T')
SIMPLE_NAMES_TO_TYPES: Dict[str, Type] = {
'int': int,
'bool': bool,
'str': str,
'float': float,
}
SIMPLE_TYPES_TO_NAMES = {tp: nm for nm, tp in SIMPLE_NAMES_TO_TYPES.items()}
def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None:
"""Safely assign values from a dict to a dataclass instance.
def dataclass_to_dict(obj: Any, coerce_to_float: bool = True) -> dict:
"""Given a dataclass object, emit a json-friendly dict.
A TypeError will be raised if types to not match the dataclass fields
or are unsupported by this function. Note that a limited number of
types are supported. More can be added as needed.
All values will be checked to ensure they match the types specified
on fields. Note that only a limited set of types is supported.
Exact types are strictly checked, so a bool cannot be passed for
an int field, an int can't be passed for a float, etc.
(can reexamine this strictness if it proves to be a problem)
An AttributeError will be raised if attributes are passed which are
not present on the dataclass as fields.
This function may add significant overhead compared to passing dict
values to a dataclass' constructor or other more direct methods, but
the increased safety checks may be worth the speed tradeoff in some
cases.
If coerce_to_float is True, integer values present on float typed fields
will be converted to floats in the dict output. If False, a TypeError
will be triggered.
"""
_dataclass_validate(instance, values)
for key, value in values.items():
setattr(instance, key, value)
out = _Outputter(obj, create=True, coerce_to_float=coerce_to_float).run()
assert isinstance(out, dict)
return out
def dataclass_validate(instance: Any) -> None:
"""Ensure values in a dataclass are correct types.
def dataclass_from_dict(cls: Type[T],
values: dict,
coerce_to_float: bool = True) -> T:
"""Given a dict, instantiates a dataclass of the given type.
Note that this will always fail if a dataclass contains field types
not supported by this module.
The dict must be in the json-friendly format as emitted from
dataclass_to_dict. This means that sequence values such as tuples or
sets should be passed as lists, enums should be passed as their
associated values, and nested dataclasses should be passed as dicts.
If coerce_to_float is True, int values passed for float typed fields
will be converted to float values. Otherwise a TypeError is raised.
"""
_dataclass_validate(instance, dataclasses.asdict(instance))
return _Inputter(cls, coerce_to_float=coerce_to_float).run(values)
def _dataclass_validate(instance: Any, values: Dict[str, Any]) -> None:
# pylint: disable=too-many-branches
if not dataclasses.is_dataclass(instance):
raise TypeError(f'Passed instance {instance} is not a dataclass.')
if not isinstance(values, dict):
raise TypeError("Expected a dict for 'values' arg.")
fields = dataclasses.fields(instance)
fieldsdict = {f.name: f for f in fields}
for key, value in values.items():
if key not in fieldsdict:
raise AttributeError(
f"'{type(instance).__name__}' has no '{key}' field.")
field = fieldsdict[key]
def dataclass_validate(obj: Any, coerce_to_float: bool = True) -> None:
"""Ensure that current values in a dataclass are the correct types."""
_Outputter(obj, create=False, coerce_to_float=coerce_to_float).run()
# We expect to be operating under 'from __future__ import annotations'
# so field types should always be strings for us; not an actual types.
# Complain if we come across an actual type.
fieldtype: str = field.type # type: ignore
if not isinstance(fieldtype, str):
raise RuntimeError(
f'Dataclass {type(instance).__name__} seems to have'
f' been created without "from __future__ import annotations";'
f' those dataclasses are unsupported here.')
if fieldtype in _SIMPLE_ASSIGN_TYPES:
reqtypes = _SIMPLE_ASSIGN_TYPES[fieldtype]
valuetype = type(value)
if not any(valuetype is t for t in reqtypes):
if len(reqtypes) == 1:
expected = reqtypes[0].__name__
else:
names = ', '.join(t.__name__ for t in reqtypes)
expected = f'Union[{names}]'
raise TypeError(f'Invalid value type for "{key}";'
f' expected "{expected}", got'
f' "{valuetype.__name__}".')
def _field_type_str(cls: Type, field: dataclasses.Field) -> str:
# We expect to be operating under 'from __future__ import annotations'
# so field types should always be strings for us; not actual types.
# (Can pull this check out once we get to Python 3.10)
typestr: str = field.type # type: ignore
elif fieldtype in _LIST_ASSIGN_TYPES:
reqtypes = _LIST_ASSIGN_TYPES[fieldtype]
if not isinstance(typestr, str):
raise RuntimeError(
f'Dataclass {cls.__name__} seems to have'
f' been created without "from __future__ import annotations";'
f' those dataclasses are unsupported here.')
return typestr
def _raise_type_error(fieldpath: str, valuetype: Type,
expected: Tuple[Type, ...]) -> None:
"""Raise an error when a field value's type does not match expected."""
assert isinstance(expected, tuple)
assert all(isinstance(e, type) for e in expected)
if len(expected) == 1:
expected_str = expected[0].__name__
else:
names = ', '.join(t.__name__ for t in expected)
expected_str = f'Union[{names}]'
raise TypeError(f'Invalid value type for "{fieldpath}";'
f' expected "{expected_str}", got'
f' "{valuetype.__name__}".')
class _Outputter:
def __init__(self, obj: Any, create: bool, coerce_to_float: bool) -> None:
self._obj = obj
self._create = create
self._coerce_to_float = coerce_to_float
def run(self) -> Any:
"""Do the thing."""
return self._dataclass_to_output(self._obj, '')
def _value_to_output(self, fieldpath: str, typestr: str,
value: Any) -> Any:
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
# For simple flat types, look for exact matches:
simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr)
if simpletype is not None:
if type(value) is not simpletype:
# Special case: if they want to coerce ints to floats, do so.
if (self._coerce_to_float and simpletype is float
and type(value) is int):
return float(value) if self._create else None
_raise_type_error(fieldpath, type(value), (simpletype, ))
return value
if typestr.startswith('Optional[') and typestr.endswith(']'):
subtypestr = typestr[9:-1]
# Handle the 'None' case special and do the default otherwise.
if value is None:
return None
return self._value_to_output(fieldpath, subtypestr, value)
if typestr.startswith('List[') and typestr.endswith(']'):
subtypestr = typestr[5:-1]
if not isinstance(value, list):
raise TypeError(
f'Invalid value for "{key}";'
f' expected a list, got a "{type(value).__name__}"')
for subvalue in value:
subvaluetype = type(subvalue)
if not any(subvaluetype is t for t in reqtypes):
if len(reqtypes) == 1:
expected = reqtypes[0].__name__
else:
names = ', '.join(t.__name__ for t in reqtypes)
expected = f'Union[{names}]'
raise TypeError(f'Invalid value type for "{key}";'
f' expected list of "{expected}", found'
f' "{subvaluetype.__name__}".')
raise TypeError(f'Expected a list for {fieldpath};'
f' found a {type(value)}')
if self._create:
return [
self._value_to_output(fieldpath, subtypestr, x)
for x in value
]
for x in value:
self._value_to_output(fieldpath, subtypestr, x)
return None
else:
raise TypeError(f'Field type "{fieldtype}" is unsupported here.')
if typestr.startswith('Set[') and typestr.endswith(']'):
subtypestr = typestr[4:-1]
if not isinstance(value, set):
raise TypeError(f'Expected a set for {fieldpath};'
f' found a {type(value)}')
if self._create:
# Note: we output json-friendly values so this becomes a list.
return [
self._value_to_output(fieldpath, subtypestr, x)
for x in value
]
for x in value:
self._value_to_output(fieldpath, subtypestr, x)
return None
if dataclasses.is_dataclass(value):
return self._dataclass_to_output(value, fieldpath)
if isinstance(value, Enum):
enumvalue = value.value
if type(enumvalue) not in SIMPLE_TYPES_TO_NAMES:
raise TypeError(f'Invalid enum value type {type(enumvalue)}'
f' for "{fieldpath}".')
return enumvalue
raise TypeError(
f"Field '{fieldpath}' of type '{typestr}' is unsupported here.")
def _dataclass_to_output(self, obj: Any, fieldpath: str) -> Any:
if not dataclasses.is_dataclass(obj):
raise TypeError(f'Passed obj {obj} is not a dataclass.')
fields = dataclasses.fields(obj)
out: Optional[Dict[str, Any]] = {} if self._create else None
for field in fields:
fieldname = field.name
if fieldpath:
subfieldpath = f'{fieldpath}.{fieldname}'
else:
subfieldpath = fieldname
typestr = _field_type_str(type(obj), field)
value = getattr(obj, fieldname)
outvalue = self._value_to_output(subfieldpath, typestr, value)
if self._create:
assert out is not None
out[fieldname] = outvalue
return out
class _Inputter(Generic[T]):
def __init__(self, cls: Type[T], coerce_to_float: bool):
self._cls = cls
self._coerce_to_float = coerce_to_float
def run(self, values: dict) -> T:
"""Do the thing."""
return self._dataclass_from_input( # type: ignore
self._cls, '', values)
def _value_from_input(self, cls: Type, fieldpath: str, typestr: str,
value: Any) -> Any:
"""Convert an assigned value to what a dataclass field expects."""
# pylint: disable=too-many-return-statements
simpletype = SIMPLE_NAMES_TO_TYPES.get(typestr)
if simpletype is not None:
if type(value) is not simpletype:
# Special case: if they want to coerce ints to floats, do so.
if (self._coerce_to_float and simpletype is float
and type(value) is int):
return float(value)
_raise_type_error(fieldpath, type(value), (simpletype, ))
return value
if typestr.startswith('List[') and typestr.endswith(']'):
return self._sequence_from_input(cls, fieldpath, typestr, value,
'List', list)
if typestr.startswith('Set[') and typestr.endswith(']'):
return self._sequence_from_input(cls, fieldpath, typestr, value,
'Set', set)
if typestr.startswith('Optional[') and typestr.endswith(']'):
subtypestr = typestr[9:-1]
# Handle the 'None' case special and do the default
# thing otherwise.
if value is None:
return None
return self._value_from_input(cls, fieldpath, subtypestr, value)
# Ok, its not a builtin type. It might be an enum or nested dataclass.
cls2 = getattr(inspect.getmodule(cls), typestr, None)
if cls2 is None:
raise RuntimeError(f"Unable to resolve '{typestr}'"
f" used by class '{cls.__name__}';"
f' make sure all nested types are declared'
f' in the global namespace of the module where'
f" '{cls.__name__} is defined.")
if dataclasses.is_dataclass(cls2):
return self._dataclass_from_input(cls2, fieldpath, value)
if issubclass(cls2, Enum):
return enum_by_value(cls2, value)
raise TypeError(
f"Field '{fieldpath}' of type '{typestr}' is unsupported here.")
def _dataclass_from_input(self, cls: Type, fieldpath: str,
values: dict) -> Any:
"""Given a dict, instantiates a dataclass of the given type.
The dict must be in the json-friendly format as emitted from
dataclass_to_dict. This means that sequence values such as tuples or
sets should be passed as lists, enums should be passed as their
associated values, and nested dataclasses should be passed as dicts.
"""
if not dataclasses.is_dataclass(cls):
raise TypeError(f'Passed class {cls} is not a dataclass.')
if not isinstance(values, dict):
raise TypeError("Expected a dict for 'values' arg.")
# noinspection PyDataclass
fields = dataclasses.fields(cls)
fields_by_name = {f.name: f for f in fields}
args: Dict[str, Any] = {}
for key, value in values.items():
field = fields_by_name.get(key)
if field is None:
raise AttributeError(f"'{cls.__name__}' has no '{key}' field.")
typestr = _field_type_str(cls, field)
subfieldpath = (f'{fieldpath}.{field.name}'
if fieldpath else field.name)
args[key] = self._value_from_input(cls, subfieldpath, typestr,
value)
return cls(**args)
def _sequence_from_input(self, cls: Type, fieldpath: str, typestr: str,
value: Any, seqtypestr: str,
seqtype: Type) -> Any:
# Because we are json-centric, we expect a list for all sequences.
if type(value) is not list:
raise TypeError(f'Invalid input value for "{fieldpath}";'
f' expected a list, got a {type(value).__name__}')
subtypestr = typestr[len(seqtypestr) + 1:-1]
return seqtype(
self._value_from_input(cls, fieldpath, subtypestr, i)
for i in value)

View File

@ -7,6 +7,7 @@ from __future__ import annotations
import datetime
import time
import weakref
from enum import Enum
from typing import TYPE_CHECKING, cast, TypeVar, Generic
if TYPE_CHECKING:
@ -18,12 +19,38 @@ T = TypeVar('T')
TVAL = TypeVar('TVAL')
TARG = TypeVar('TARG')
TRET = TypeVar('TRET')
TENUM = TypeVar('TENUM', bound=Enum)
class _EmptyObj:
pass
def enum_by_value(cls: Type[TENUM], value: Any) -> TENUM:
"""Create an enum from a value.
This is basically the same as doing 'obj = EnumType(value)' except
that it works around an issue where a reference loop is created
if an exception is thrown due to an invalid value. Since we disable
the cyclic garbage collector for most of the time, such loops can lead
to our objects sticking around longer than we want.
This issue has been submitted to Python as a bug so hopefully we can
remove this eventually if it gets fixed: https://bugs.python.org/issue42248
"""
# Note: we don't recreate *ALL* the functionality of the Enum constructor
# such as the _missing_ hook; but this should cover our basic needs.
value2member_map = getattr(cls, '_value2member_map_')
assert value2member_map is not None
try:
out = value2member_map[value]
assert isinstance(out, cls)
return out
except KeyError:
raise ValueError('%r is not a valid %s' %
(value, cls.__name__)) from None
def utc_now() -> datetime.datetime:
"""Get offset-aware current utc time.