diff --git a/.efrocachemap b/.efrocachemap index 37c97bd8..f734b27e 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3966,50 +3966,50 @@ "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/72/82/86956fae909ac2fe2a1abd84a361", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", - "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e0/06/60b8a1d7a19049a169c2b68cf89e", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/15/fe/006539c400523428bb45ce4da9b2", "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/74/1d/fc9e33e565475daaac80da5252f0", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ff/56/a8f14d13ba81f23800568720624d", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/39/11/72a9a331f96f4bca715374dbf242", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/00/a8/92d6a00c4af3c43e0af4bbe7b143", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/51/6f/40639d4d24908fbd9c32dd781818", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/60/8f/996fdf4a1f1e26b566b5e0b4f54b", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/bc/75/418515fb999d524564ba485e3643", "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/9c/7b/ac1a200be0f37078af0991faca3b", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1a/83/3884b71fc3c83a597114c661fc94", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/be/98/c29dc5249a7d772f778de4ca4119", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c8/2f/07bae56da30dd2a4eb549d21788a", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/fd/cf88baa3715276b66ffc8e500c28", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cd/86/75a814002acbd4e3e7ce72135a69", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/9a/90efe486fbe7015fc45d71215350", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/34/81/45129983739ba3ab4c3b42aa6039", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/5e/10/d0a79286b205c0da36f0a05b289c", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/20/9e/2798a0a74cf535e1cccb4e2feb00", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/95/55/eefd6a0c57c7a1f5d84128d3c1de", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f2/9f/d5b12184218d6f15743d22e34ecf", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/34/b9/0da9f3ac3d89cf144130ed5d6032", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/bb/a3/8ee9ae4cd5260306bb14a17792ea", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/79/79/5b30b573a258262cf7ab5247b05d", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a3/d7/80ac73303ca83f1768b9ce1c2078", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f5/2a/c618f4f733d32d8a583348042e20", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/63/6a/d187d352844a3b7c1861e79d2c8f", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/bb/52/ae0ed2e83a449865252d90c2ac9a", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/be/2e/4bc4bf1ec4db1b98c7d7081095d4", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/59/7e/253051aa3d105e94235f74a0b5ae", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/65/63cf06b0f8bd7fbc15edaf8a842b", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/63/61/7ff8074fa76ae8b5010b07d4f1f2", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5a/31/9b542804ba449ce4c8c6eddf3971", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3c/38/6be5cd846d6bd38acea70e8ee667", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2b/d9/7c24f63dd8737ca4ecb77d103560", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/70/2c/1d44685db575e201542685f41f32", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/82/97/2afbffeaf0989dfd5094a48ecac7", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d4/2c/d5cdded5a9d1018dad9f8e8a812e", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ac/47/16da91f514af302c5435d060d75c", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/67/5b/47db352ef527391482b42e1072e8", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/9f/1d/3d2c2056c491fdab1055a6a5f114", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/1e/a2/ff5dd453f396fa58362e24b34b5f", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/30/8b/3a547ac0ce167a0c43492dab5104", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/73/8d/d25e325791ac3e0aab48f204bea3", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/cd/a4/925f8c8ef4026b07b55ef841e8e8", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/62/b9/3539b66b55f11851633a120b40a1", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/64/d6/57d647611e82b6b36b0c2decd826", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/f7/2f/f34dd67c2d40a8988b999cb59d2e", - "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/f2/6c/5a0a4695dcc2a11e7941b8777e80", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/47/d4/1d346b91ebcaa1215954d9a2bb71", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b0/c3/7c1cb5a0f96212ceafc08f71369a", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5b/c9/3397972da39e948b812bc809e0c1", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/32/51/ff535d9b0f91f0defc7f9aee2bc4", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ca/01/ecda0f5771d5008da48e7328da34", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6c/6d/93aa177977ed7d0c529f4bcaa212", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/3d/d25cb717747c1f41a64c3dc2b353", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c4/3a/64a33f5f837c435fd61bc34621de", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/20/0f/157782b569ef6dfbe23e5f435aeb", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a6/f6/f1a09d846273291efcdebc32384c", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e1/23/154b39f6ce4017f694b9bc50b4e5", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/19/42/ffe1fbc5277708393c96ae1556b2", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/c2/81/1445b40155e83482fe8629fc4659", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/09/88/cc1f4f48e2c3f98bd9140507aca4", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/95/51/acefa044dba77f3adada7115f487", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ca/18/846e182605d341b8225865c8646c", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/71/61/f81984085eb141f9ea1e6abca272", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f0/3c/2820206454ed96ae7317eb3ff31f", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/52/68/e27ec9c0b2253213dd24cd49701c", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/83/f5/9c21b5431f1f22bad111b0872301", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/80/82/6af017a24c9de0e67d1702b538e6", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ad/00/be95b2212c2b2555c4c5130a0cd0", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8c/e2/5c704673f29a89445eee67248f84", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cc/1c/91c9f1ddf9c159f63a45175596eb", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/36/3b/250d4aaaccfcb8d60c4fbf3f083d", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/2b/59466f90e18c090c8a9154b38b7e", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d7/6d/f22ddf7ee877b50c3010506bff49", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d9/b2/34b5f247b3952323e4ffde3d4a2d", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ad/fb/5ccdb9da44706867aa56cb0c6316", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d9/02/36ab4592b9a994e9be386c79d2df", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/af/1a/85c3b9a25c3cabe0cf05892b34ee", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/3e/6a/a12f9aad3dc9de538d7f99751709", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/01/c5/fe63c070acfd6592a41a0a1f6480", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/2d/0a/9594864542e84204caba36de407c", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ae/42/17489667000e866dfa319d84cb04", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/1a/c4/a5ad68b0e99db6f802b7bdb3cb04", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/e2/70/bff2f5ab3526af5a39e6ac4a65cd", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/3b/7c/d9e5ae045c3347812a30f75273e5", + "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/c5/18/29d9fe8e483ce222d3263336f7e6", "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/65/ac/d5c4162a71028c1bfa73ebc1f881" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index c98ecec1..ed2a14df 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -524,6 +524,7 @@ dbpath dcioexattrs dcioprep + dcioprepsession dcls dcmake deathmatch @@ -1086,6 +1087,7 @@ incmd incr incrementbuild + indata indentfilter indentstr indexfile @@ -1966,6 +1968,7 @@ rsdr rsms rstr + rtest rtnetlink rtxt rtypes @@ -2284,6 +2287,7 @@ tcall tchar tclass + tcls tcombine tdelay tdval diff --git a/CHANGELOG.md b/CHANGELOG.md index 4700233a..6d79c1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 1.6.7 (20394) +- Fixed a vulnerability which could expose device-account uuids. + ### 1.6.6 (20394) - Beginning work on moving to new asset system. - Added Tamil language (Thanks Ryan!) diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash index ca745646..e00d6c53 100644 --- a/assets/src/ba_data/python/._ba_sources_hash +++ b/assets/src/ba_data/python/._ba_sources_hash @@ -1 +1 @@ -128243803030405974762870876563323013389 \ No newline at end of file +142084453862763162635228312125641718455 \ No newline at end of file diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index 7860d1e5..11dd8235 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -119,6 +119,11 @@ def gear_vr_controller_warning() -> None: color=(1, 0, 0)) +def uuid_str() -> str: + import uuid + return str(uuid.uuid4()) + + def orientation_reset_cb_message() -> None: from ba._language import Lstr _ba.screenmessage( diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index ca13bf7c..905d9005 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -248,6 +248,7 @@ dbias dcioexattrs dcioprep + dcioprepsession dcol ddcaps ddpf @@ -513,6 +514,7 @@ imagewidget importlines incentivized + indata inet infotxt inides @@ -935,6 +937,7 @@ rresult rscode rsgc + rtest rtypes rtypevar runnables @@ -1073,6 +1076,7 @@ tabtypes talloc targs + tcls tegra telefonaktiebolaget teleported diff --git a/docs/ba_module.md b/docs/ba_module.md index 7f8a66f6..48598500 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2021-12-22 for Ballistica version 1.6.6 build 20416

+

last updated on 2022-01-25 for Ballistica version 1.6.7 build 20427

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 let me know. Happy modding!


diff --git a/src/ballistica/app/app_globals.h b/src/ballistica/app/app_globals.h index ffaa368c..87689a1f 100644 --- a/src/ballistica/app/app_globals.h +++ b/src/ballistica/app/app_globals.h @@ -54,7 +54,7 @@ class AppGlobals { bool reset_vr_orientation{}; bool user_ran_commands{}; UIScale ui_scale{UIScale::kLarge}; - AccountType account_type{AccountType::kInvalid}; + V1AccountType account_type{V1AccountType::kInvalid}; bool remote_server_accepting_connections{true}; std::string exec_command; std::string user_agent_string{"BA_USER_AGENT_UNSET (" BA_PLATFORM_STRING ")"}; diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 323e6ec7..3d16e60d 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -21,8 +21,8 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20416; -const char* kAppVersion = "1.6.6"; +const int kAppBuildNumber = 20427; +const char* kAppVersion = "1.6.7"; // Our standalone globals. // These are separated out for easy access. @@ -213,13 +213,24 @@ auto FatalError(const std::string& message) -> void { auto GetUniqueSessionIdentifier() -> const std::string& { static std::string session_id; static bool have_session_id = false; + if (!have_session_id) { - srand(static_cast( - Platform::GetCurrentMilliseconds())); // NOLINT - auto tval = static_cast(rand()); // NOLINT - assert(g_platform); - session_id = g_platform->GetUniqueDeviceIdentifier() + std::to_string(tval); - have_session_id = true; + if (g_python) { + Python::ScopedInterpreterLock gil; + auto uuid = g_python->obj(Python::ObjID::kUUIDStrCall).Call(); + if (uuid.exists()) { + session_id = uuid.ValueAsString().c_str(); + have_session_id = true; + } + } + if (!have_session_id) { + // As an emergency fallback simply use a single random number. + Log("WARNING: GetUniqueSessionIdentifier() using rand fallback."); + srand(static_cast( + Platform::GetCurrentMilliseconds())); // NOLINT + session_id = std::to_string(static_cast(rand())); // NOLINT + have_session_id = true; + } if (session_id.size() >= 100) { Log("WARNING: session id longer than it should be."); } diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h index a6a3e7d3..5ad44dfb 100644 --- a/src/ballistica/core/types.h +++ b/src/ballistica/core/types.h @@ -852,7 +852,7 @@ enum class NodeMessageType { kFooting }; -enum class LoginState { kSignedOut, kSigningIn, kSignedIn }; +enum class V1LoginState { kSignedOut, kSigningIn, kSignedIn }; enum class CameraMode { kFollow, kOrbit }; @@ -1007,7 +1007,7 @@ enum class ThreadIdentifier { kBGDynamics }; -enum class AccountType { +enum class V1AccountType { kInvalid, kTest, kGameCenter, diff --git a/src/ballistica/game/account.h b/src/ballistica/game/account.h index 502670f5..099e2ca2 100644 --- a/src/ballistica/game/account.h +++ b/src/ballistica/game/account.h @@ -16,9 +16,9 @@ namespace ballistica { class Account { public: Account(); - static auto AccountTypeFromString(const std::string& val) -> AccountType; - static auto AccountTypeToString(AccountType type) -> std::string; - static auto AccountTypeToIconString(AccountType type) -> std::string; + static auto AccountTypeFromString(const std::string& val) -> V1AccountType; + static auto AccountTypeToString(V1AccountType type) -> std::string; + static auto AccountTypeToIconString(V1AccountType type) -> std::string; auto GetLoginName() -> std::string; auto GetLoginID() -> std::string; @@ -28,7 +28,7 @@ class Account { /// Return the current account state. /// If an int pointer is passed, state-num will also be returned. - auto GetLoginState(int* state_num = nullptr) -> LoginState; + auto GetLoginState(int* state_num = nullptr) -> V1LoginState; // An extra value included when passing our account info to the server // ...(can be used for platform-specific install-signature stuff, etc.). @@ -37,7 +37,7 @@ class Account { auto SetToken(const std::string& account_id, const std::string& token) -> void; - auto SetLogin(AccountType account_type, LoginState login_state, + auto SetLogin(V1AccountType account_type, V1LoginState login_state, const std::string& login_name, const std::string& login_id) -> void; @@ -57,7 +57,7 @@ class Account { std::string token_; std::string extra_; std::string extra_2_; - LoginState login_state_{LoginState::kSignedOut}; + V1LoginState login_state_{V1LoginState::kSignedOut}; int login_state_num_{}; }; diff --git a/src/ballistica/game/game.cc b/src/ballistica/game/game.cc index 6bf8cefa..8613f760 100644 --- a/src/ballistica/game/game.cc +++ b/src/ballistica/game/game.cc @@ -253,14 +253,10 @@ void Game::PushMediaPruneCall(int level) { }); } -void Game::PushSetAccountTokenCall(const std::string& account_id, - const std::string& token) { - PushCall([account_id, token] { g_account->SetToken(account_id, token); }); -} - -void Game::PushSetLoginCall(AccountType account_type, LoginState account_state, - const std::string& account_name, - const std::string& account_id) { +void Game::PushSetV1LoginCall(V1AccountType account_type, + V1LoginState account_state, + const std::string& account_name, + const std::string& account_id) { PushCall([this, account_type, account_state, account_name, account_id] { g_account->SetLogin(account_type, account_state, account_name, account_id); }); @@ -964,7 +960,6 @@ void Game::PushInterruptSignalCall() { return; } - // Just go through _ba.quit() // FIXME: Shouldn't need to go out to the Python layer here... g_python->obj(Python::ObjID::kQuitCall).Call(); }); @@ -979,11 +974,10 @@ void Game::PushAskUserForTelnetAccessCall() { } void Game::HandleThreadPause() { - // Give userspace python stuff a chance to pause. ScopedSetContext cp(GetUIContextTarget()); - g_python->obj(Python::ObjID::kOnAppPauseCall).Call(); - // Tell our account client to commit any outstanding changes to disk. + // Let Python and internal layers do their thing. + g_python->obj(Python::ObjID::kOnAppPauseCall).Call(); AppInternalOnGameThreadPause(); } @@ -1831,23 +1825,24 @@ void Game::CleanUpBeforeConnectingToHost() { SetPublicPartyEnabled(false); } -void Game::PushPartyInviteCall(const std::string& name, - const std::string& invite_id) { - PushCall([this, name, invite_id] { PartyInvite(name, invite_id); }); +void Game::PushV1PartyInviteCall(const std::string& name, + const std::string& invite_id) { + PushCall([this, name, invite_id] { V1PartyInvite(name, invite_id); }); } -void Game::PartyInvite(const std::string& name, const std::string& invite_id) { +void Game::V1PartyInvite(const std::string& name, + const std::string& invite_id) { assert(InGameThread()); - g_python->PartyInvite(name, invite_id); + g_python->V1PartyInvite(name, invite_id); } -void Game::PushPartyInviteRevokeCall(const std::string& invite_id) { - PushCall([this, invite_id] { PartyInviteRevoke(invite_id); }); +void Game::PushV1PartyInviteRevokeCall(const std::string& invite_id) { + PushCall([this, invite_id] { V1PartyInviteRevoke(invite_id); }); } -void Game::PartyInviteRevoke(const std::string& invite_id) { +void Game::V1PartyInviteRevoke(const std::string& invite_id) { assert(InGameThread()); - g_python->PartyInviteRevoke(invite_id); + g_python->V1PartyInviteRevoke(invite_id); } auto Game::GetPartySize() const -> int { diff --git a/src/ballistica/game/game.h b/src/ballistica/game/game.h index d6120248..a298166a 100644 --- a/src/ballistica/game/game.h +++ b/src/ballistica/game/game.h @@ -32,14 +32,13 @@ class Game : public Module { auto LaunchClientSession() -> void; auto LaunchReplaySession(const std::string& file_name) -> void; - auto PushSetLoginCall(AccountType account_type, LoginState account_state, - const std::string& account_name, - const std::string& account_id) -> void; - auto PushSetAccountTokenCall(const std::string& account_id, - const std::string& token) -> void; - auto PushPartyInviteCall(const std::string& name, - const std::string& invite_id) -> void; - auto PushPartyInviteRevokeCall(const std::string& invite_id) -> void; + auto PushSetV1LoginCall(V1AccountType account_type, + V1LoginState account_state, + const std::string& account_name, + const std::string& account_id) -> void; + auto PushV1PartyInviteCall(const std::string& name, + const std::string& invite_id) -> void; + auto PushV1PartyInviteRevokeCall(const std::string& invite_id) -> void; auto PushInitialScreenCreatedCall() -> void; auto PushApplyConfigCall() -> void; auto PushRemoveGraphicsServerRenderHoldCall() -> void; @@ -253,9 +252,9 @@ class Game : public Module { auto HandleQuitOnIdle() -> void; auto InitSpecialChars() -> void; auto Draw() -> void; - auto PartyInvite(const std::string& name, const std::string& invite_id) + auto V1PartyInvite(const std::string& name, const std::string& invite_id) -> void; - auto PartyInviteRevoke(const std::string& invite_id) -> void; + auto V1PartyInviteRevoke(const std::string& invite_id) -> void; auto InitialScreenCreated() -> void; auto MainMenuPress(InputDevice* device) -> void; auto ScreenResize(float virtual_width, float virtual_height, diff --git a/src/ballistica/game/player_spec.cc b/src/ballistica/game/player_spec.cc index ac685d54..5d7bea9a 100644 --- a/src/ballistica/game/player_spec.cc +++ b/src/ballistica/game/player_spec.cc @@ -34,7 +34,7 @@ PlayerSpec::PlayerSpec(const std::string& s) { Log("Error creating PlayerSpec from string: '" + s + "'"); name_ = ""; short_name_ = ""; - account_type_ = AccountType::kInvalid; + account_type_ = V1AccountType::kInvalid; } } @@ -75,7 +75,7 @@ auto PlayerSpec::GetSpecString() const -> std::string { auto PlayerSpec::GetAccountPlayerSpec() -> PlayerSpec { PlayerSpec spec; - if (g_account->GetLoginState() == LoginState::kSignedIn) { + if (g_account->GetLoginState() == V1LoginState::kSignedIn) { spec.account_type_ = g_app_globals->account_type; spec.name_ = Utils::GetValidUTF8(g_account->GetLoginName().c_str(), "bsgaps"); diff --git a/src/ballistica/game/player_spec.h b/src/ballistica/game/player_spec.h index 4292cd09..a2b0cffd 100644 --- a/src/ballistica/game/player_spec.h +++ b/src/ballistica/game/player_spec.h @@ -48,7 +48,7 @@ class PlayerSpec { private: std::string name_; std::string short_name_; - AccountType account_type_{AccountType::kInvalid}; + V1AccountType account_type_{V1AccountType::kInvalid}; }; } // namespace ballistica diff --git a/src/ballistica/platform/platform.h b/src/ballistica/platform/platform.h index 061513da..7f68738f 100644 --- a/src/ballistica/platform/platform.h +++ b/src/ballistica/platform/platform.h @@ -153,10 +153,12 @@ class Platform { // Return a string *reasonably* likely to be unique and consistent for this // device. Do not assume this is globally unique and *do not* assume that it // will never ever change (hardware upgrades may affect it, etc). + // IMPORTANT: This value should NEVER be sent over the wire to peers. virtual auto GetUniqueDeviceIdentifier() -> const std::string&; // Returns the ID to use for the device account auto GetDeviceAccountID() -> std::string; + auto GetConfigDirectory() -> std::string; auto GetConfigFilePath() -> std::string; auto GetUserPythonDirectory() -> std::string; diff --git a/src/ballistica/python/python.cc b/src/ballistica/python/python.cc index 86ab60f4..668496f9 100644 --- a/src/ballistica/python/python.cc +++ b/src/ballistica/python/python.cc @@ -2322,8 +2322,8 @@ auto Python::ObjToString(PyObject* obj) -> std::string { } } -void Python::PartyInvite(const std::string& player, - const std::string& invite_id) { +void Python::V1PartyInvite(const std::string& player, + const std::string& invite_id) { ScopedSetContext cp(g_game->GetUIContext()); PythonRef args( Py_BuildValue( @@ -2336,7 +2336,7 @@ void Python::PartyInvite(const std::string& player, obj(ObjID::kHandlePartyInviteCall).Call(args); } -void Python::PartyInviteRevoke(const std::string& invite_id) { +void Python::V1PartyInviteRevoke(const std::string& invite_id) { ScopedSetContext cp(g_game->GetUIContext()); PythonRef args( Py_BuildValue("(O)", PythonRef(PyUnicode_FromString(invite_id.c_str()), diff --git a/src/ballistica/python/python.h b/src/ballistica/python/python.h index fd551202..343934b0 100644 --- a/src/ballistica/python/python.h +++ b/src/ballistica/python/python.h @@ -142,8 +142,8 @@ class Python { /// is useful as an object identifier/etc. static auto GetPythonFileLocation(bool pretty = true) -> std::string; - void PartyInvite(const std::string& player, const std::string& invite_id); - void PartyInviteRevoke(const std::string& invite_id); + void V1PartyInvite(const std::string& player, const std::string& invite_id); + void V1PartyInviteRevoke(const std::string& invite_id); void set_env_obj(PyObject* obj) { env_ = obj; } auto env_obj() const -> PyObject* { assert(env_); @@ -351,6 +351,7 @@ class Python { kPlayerClass, kGetPlayerIconCall, kLstrFromJsonCall, + kUUIDStrCall, kLast // Sentinel; must be at end. }; diff --git a/src/meta/bameta/python_embedded/binding.py b/src/meta/bameta/python_embedded/binding.py index 4ef39ca1..bcb7bf65 100644 --- a/src/meta/bameta/python_embedded/binding.py +++ b/src/meta/bameta/python_embedded/binding.py @@ -134,4 +134,5 @@ def get_binding_values() -> tuple[Any, ...]: _player.Player, # kPlayerClass _hooks.get_player_icon, # kGetPlayerIconCall _language.Lstr.from_json, # kLstrFromJsonCall + _hooks.uuid_str, # kUUIDStrCall ) # yapf: disable diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py index 5b53a1a0..28b7496f 100644 --- a/tests/test_efro/test_dataclassio.py +++ b/tests/test_efro/test_dataclassio.py @@ -13,8 +13,8 @@ import pytest from efro.util import utc_now from efro.dataclassio import (dataclass_validate, dataclass_from_dict, - dataclass_to_dict, ioprepped, IOAttrs, Codec, - DataclassFieldLookup) + dataclass_to_dict, ioprepped, ioprep, IOAttrs, + Codec, DataclassFieldLookup, IOExtendedData) if TYPE_CHECKING: pass @@ -220,11 +220,11 @@ def test_assign() -> None: dataclass_from_dict(_TestClass, {'ssval': {}}) with pytest.raises(TypeError): dataclass_from_dict(_TestClass, {'ssval': set()}) - with pytest.raises(TypeError): + with pytest.raises(ValueError): dataclass_from_dict(_TestClass, {'tupleval': []}) with pytest.raises(TypeError): dataclass_from_dict(_TestClass, {'tupleval': [1, 1, 1]}) - with pytest.raises(TypeError): + with pytest.raises(ValueError): dataclass_from_dict(_TestClass, {'tupleval': [2, 'foof', True, True]}) # Fields with type Any should accept all types which are directly @@ -652,6 +652,36 @@ def test_name_clashes() -> None: ival2: Annotated[int, IOAttrs('ival')] = 5 +@dataclass +class _RecursiveTest: + val: int + child: Optional[_RecursiveTest] = None + + +def test_recursive() -> None: + """Test recursive classes.""" + + # Can't use ioprepped on this since it refers to its own name which + # doesn't exist yet. Have to explicitly prep it after. + ioprep(_RecursiveTest) + + rtest = _RecursiveTest(val=1) + rtest.child = _RecursiveTest(val=2) + rtest.child.child = _RecursiveTest(val=3) + expected_output = { + 'val': 1, + 'child': { + 'val': 2, + 'child': { + 'val': 3, + 'child': None + } + } + } + assert dataclass_to_dict(rtest) == expected_output + assert dataclass_from_dict(_RecursiveTest, expected_output) == rtest + + def test_any() -> None: """Test data included with type Any.""" @@ -795,3 +825,51 @@ def test_nested() -> None: subval: _TestSubClass = field(default_factory=_TestSubClass) enval: _TestEnum = _TestEnum.VAL1 + + +def test_extended_data() -> None: + """Test IOExtendedData functionality.""" + + @ioprepped + @dataclass + class _TestClass: + vals: tuple[int, int] + + # This data lines up. + indata = {'vals': [0, 0]} + _obj = dataclass_from_dict(_TestClass, indata) + + # This data doesn't. + indata = {'vals': [0, 0, 0]} + with pytest.raises(ValueError): + _obj = dataclass_from_dict(_TestClass, indata) + + # Now define the same data but give it an adapter + # so it can work with our incorrectly-formatted data. + @ioprepped + @dataclass + class _TestClass2(IOExtendedData): + vals: tuple[int, int] + + @classmethod + def will_input(cls, data: dict) -> None: + data['vals'] = data['vals'][:2] + + def will_output(self) -> None: + self.vals = (0, 0) + + # This data lines up. + indata = {'vals': [0, 0]} + _obj2 = dataclass_from_dict(_TestClass2, indata) + + # Now this data will too via our custom input filter. + indata = {'vals': [0, 0, 0]} + _obj2 = dataclass_from_dict(_TestClass2, indata) + + # Ok, now test output: + + # Does the expected thing. + assert dataclass_to_dict(_TestClass(vals=(1, 2))) == {'vals': [1, 2]} + + # Uses our output filter. + assert dataclass_to_dict(_TestClass2(vals=(1, 2))) == {'vals': [0, 0]} diff --git a/tools/bacommon/assets.py b/tools/bacommon/assets.py index 311852d4..097b52f6 100644 --- a/tools/bacommon/assets.py +++ b/tools/bacommon/assets.py @@ -38,8 +38,8 @@ class AssetType(Enum): @dataclass class AssetPackageFlavorManifest: """A manifest of asset info for a specific flavor of an asset package.""" - assetfiles: Annotated[dict[str, str], - IOAttrs('assetfiles')] = field(default_factory=dict) + cloudfiles: Annotated[dict[str, str], + IOAttrs('cloudfiles')] = field(default_factory=dict) @ioprepped diff --git a/tools/efro/dataclassio/__init__.py b/tools/efro/dataclassio/__init__.py index b11f23ea..ac9e6c2a 100644 --- a/tools/efro/dataclassio/__init__.py +++ b/tools/efro/dataclassio/__init__.py @@ -15,17 +15,19 @@ from typing import TYPE_CHECKING, TypeVar from efro.dataclassio._outputter import _Outputter from efro.dataclassio._inputter import _Inputter -from efro.dataclassio._base import Codec, IOAttrs -from efro.dataclassio._prep import ioprep, ioprepped, is_ioprepped_dataclass +from efro.dataclassio._base import Codec, IOAttrs, IOExtendedData +from efro.dataclassio._prep import (ioprep, ioprepped, will_ioprep, + is_ioprepped_dataclass) from efro.dataclassio._pathcapture import DataclassFieldLookup if TYPE_CHECKING: from typing import Any, Optional __all__ = [ - 'Codec', 'IOAttrs', 'ioprep', 'ioprepped', 'is_ioprepped_dataclass', - 'DataclassFieldLookup', 'dataclass_to_dict', 'dataclass_to_json', - 'dataclass_from_dict', 'dataclass_from_json', 'dataclass_validate' + 'Codec', 'IOAttrs', 'IOExtendedData', 'ioprep', 'ioprepped', 'will_ioprep', + 'is_ioprepped_dataclass', 'DataclassFieldLookup', 'dataclass_to_dict', + 'dataclass_to_json', 'dataclass_from_dict', 'dataclass_from_json', + 'dataclass_validate' ] T = TypeVar('T') diff --git a/tools/efro/dataclassio/_base.py b/tools/efro/dataclassio/_base.py index 6240918b..1353c36b 100644 --- a/tools/efro/dataclassio/_base.py +++ b/tools/efro/dataclassio/_base.py @@ -12,15 +12,6 @@ from typing import TYPE_CHECKING, get_args # noinspection PyProtectedMember from typing import _AnnotatedAlias # type: ignore -_pytz_utc: Any - -# We don't *require* pytz, but we want to support it for tzinfos if available. -try: - import pytz - _pytz_utc = pytz.utc -except ModuleNotFoundError: - _pytz_utc = None # pylint: disable=invalid-name - if TYPE_CHECKING: from typing import Any, Optional @@ -32,14 +23,6 @@ SIMPLE_TYPES = {int, bool, str, float, type(None)} EXTRA_ATTRS_ATTR = '_DCIOEXATTRS' -def _ensure_datetime_is_timezone_aware(value: datetime.datetime) -> None: - # We only support timezone-aware utc times. - if (value.tzinfo is not datetime.timezone.utc - and (_pytz_utc is None or value.tzinfo is not _pytz_utc)): - raise ValueError( - 'datetime values must have timezone set as timezone.utc') - - 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.""" @@ -67,6 +50,24 @@ class Codec(Enum): FIRESTORE = 'firestore' +class IOExtendedData: + """A class that data types can inherit from for extra functionality.""" + + def will_output(self) -> None: + """Called before data is sent to an outputter. + + Can be overridden to validate or filter data before + sending it on its way. + """ + + @classmethod + def will_input(cls, data: dict) -> None: + """Called on raw data before a class instance is created from it. + + Can be overridden to migrate old data formats to new, etc. + """ + + def _is_valid_for_codec(obj: Any, codec: Codec) -> bool: """Return whether a value consists solely of json-supported types. diff --git a/tools/efro/dataclassio/_inputter.py b/tools/efro/dataclassio/_inputter.py index dfa0c9b0..274e1681 100644 --- a/tools/efro/dataclassio/_inputter.py +++ b/tools/efro/dataclassio/_inputter.py @@ -14,11 +14,11 @@ import typing import datetime from typing import TYPE_CHECKING, Generic, TypeVar -from efro.util import enum_by_value +from efro.util import enum_by_value, check_utc from efro.dataclassio._base import (Codec, _parse_annotated, EXTRA_ATTRS_ATTR, _is_valid_for_codec, _get_origin, SIMPLE_TYPES, _raise_type_error, - _ensure_datetime_is_timezone_aware) + IOExtendedData) from efro.dataclassio._prep import PrepSession if TYPE_CHECKING: @@ -48,6 +48,12 @@ class _Inputter(Generic[T]): def run(self, values: dict) -> T: """Do the thing.""" + + # For special extended data types, call their 'will_output' callback. + tcls = self._cls + if issubclass(tcls, IOExtendedData): + tcls.will_input(values) + out = self._dataclass_from_input(self._cls, '', values) assert isinstance(out, self._cls) return out @@ -159,6 +165,7 @@ class _Inputter(Generic[T]): prep = PrepSession(explicit=False).prep_dataclass(cls, recursion_level=0) + assert prep is not None extra_attrs = {} @@ -344,7 +351,7 @@ class _Inputter(Generic[T]): f'Invalid input value for "{fieldpath}" on' f' "{cls.__name__}";' f' expected a datetime, got a {type(value).__name__}') - _ensure_datetime_is_timezone_aware(value) + check_utc(value) return value assert self._codec is Codec.JSON @@ -355,9 +362,9 @@ class _Inputter(Generic[T]): f'Invalid input value for "{fieldpath}" on "{cls.__name__}";' f' expected a list, got a {type(value).__name__}') if len(value) != 7 or not all(isinstance(x, int) for x in value): - raise TypeError( + raise ValueError( f'Invalid input value for "{fieldpath}" on "{cls.__name__}";' - f' expected a list of 7 ints.') + f' expected a list of 7 ints, got {[type(v) for v in value]}.') out = datetime.datetime( # type: ignore *value, tzinfo=datetime.timezone.utc) if ioattrs is not None: @@ -380,9 +387,9 @@ class _Inputter(Generic[T]): assert childanntypes if len(value) != len(childanntypes): - raise TypeError(f'Invalid tuple input for "{fieldpath}";' - f' expected {len(childanntypes)} values,' - f' found {len(value)}.') + raise ValueError(f'Invalid tuple input for "{fieldpath}";' + f' expected {len(childanntypes)} values,' + f' found {len(value)}.') for i, childanntype in enumerate(childanntypes): childval = value[i] diff --git a/tools/efro/dataclassio/_outputter.py b/tools/efro/dataclassio/_outputter.py index 9069bc08..746223db 100644 --- a/tools/efro/dataclassio/_outputter.py +++ b/tools/efro/dataclassio/_outputter.py @@ -14,10 +14,11 @@ import typing import datetime from typing import TYPE_CHECKING +from efro.util import check_utc from efro.dataclassio._base import (Codec, _parse_annotated, EXTRA_ATTRS_ATTR, _is_valid_for_codec, _get_origin, SIMPLE_TYPES, _raise_type_error, - _ensure_datetime_is_timezone_aware) + IOExtendedData) from efro.dataclassio._prep import PrepSession if TYPE_CHECKING: @@ -37,6 +38,11 @@ class _Outputter: def run(self) -> Any: """Do the thing.""" + + # For special extended data types, call their 'will_output' callback. + if isinstance(self._obj, IOExtendedData): + self._obj.will_output() + return self._process_dataclass(type(self._obj), self._obj, '') def _process_dataclass(self, cls: type, obj: Any, fieldpath: str) -> Any: @@ -44,6 +50,7 @@ class _Outputter: # pylint: disable=too-many-branches prep = PrepSession(explicit=False).prep_dataclass(type(obj), recursion_level=0) + assert prep is not None fields = dataclasses.fields(obj) out: Optional[dict[str, Any]] = {} if self._create else None for field in fields: @@ -242,7 +249,7 @@ class _Outputter: if not isinstance(value, origin): raise TypeError(f'Expected a {origin} for {fieldpath};' f' found a {type(value)}.') - _ensure_datetime_is_timezone_aware(value) + check_utc(value) if ioattrs is not None: ioattrs.validate_datetime(value, fieldpath) if self._codec is Codec.FIRESTORE: diff --git a/tools/efro/dataclassio/_pathcapture.py b/tools/efro/dataclassio/_pathcapture.py index 3d90647b..af7113f9 100644 --- a/tools/efro/dataclassio/_pathcapture.py +++ b/tools/efro/dataclassio/_pathcapture.py @@ -36,6 +36,7 @@ class _PathCapture: prep = PrepSession(explicit=False).prep_dataclass(self._cls, recursion_level=0) + assert prep is not None try: anntype = prep.annotations[name] except KeyError as exc: diff --git a/tools/efro/dataclassio/_prep.py b/tools/efro/dataclassio/_prep.py index 7cc73512..eec61c57 100644 --- a/tools/efro/dataclassio/_prep.py +++ b/tools/efro/dataclassio/_prep.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, TypeVar, get_type_hints from efro.dataclassio._base import _parse_annotated, _get_origin, SIMPLE_TYPES if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional T = TypeVar('T') @@ -27,9 +27,13 @@ T = TypeVar('T') # (basically for detecting recursive types) MAX_RECURSION = 10 -# Attr name for data we store on dataclass types as part of prep. +# Attr name for data we store on dataclass types that have been prepped. PREP_ATTR = '_DCIOPREP' +# We also store the prep-session while the prep is in progress. +# (necessary to support recursive types). +PREP_SESSION_ATTR = '_DCIOPREPSESSION' + def ioprep(cls: type) -> None: """Prep a dataclass type for use with this module's functionality. @@ -64,6 +68,23 @@ def ioprepped(cls: type[T]) -> type[T]: return cls +def will_ioprep(cls: type[T]) -> type[T]: + """Class decorator hinting that we will prep a class later. + + In some cases (such as recursive types) we cannot use the @ioprepped + decorator and must instead call ioprep() explicitly later. However, + some of our custom pylint checking behaves differently when the + @ioprepped decorator is present, in that case requiring type annotations + to be present and not simply forward declared under an "if TYPE_CHECKING" + block. (since they are used at runtime). + + The @will_ioprep decorator triggers the same pylint behavior + differences as @ioprepped (which are necessary for the later ioprep() call + to work correctly) but without actually running any prep itself. + """ + return cls + + def is_ioprepped_dataclass(obj: Any) -> bool: """Return whether the obj is an ioprepped dataclass type or instance.""" cls = obj if isinstance(obj, type) else type(obj) @@ -90,8 +111,15 @@ class PrepSession: def __init__(self, explicit: bool): self.explicit = explicit - def prep_dataclass(self, cls: type, recursion_level: int) -> PrepData: - """Run prep on a dataclass if necessary and return its prep data.""" + def prep_dataclass(self, cls: type, + recursion_level: int) -> Optional[PrepData]: + """Run prep on a dataclass if necessary and return its prep data. + + The only case where this will return None is for recursive types + if the type is already being prepped higher in the call order. + """ + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches # We should only need to do this once per dataclass. existing_data = getattr(cls, PREP_ATTR, None) @@ -99,8 +127,9 @@ class PrepSession: assert isinstance(existing_data, PrepData) return existing_data - # If we run into classes containing themselves, we may have - # to do something smarter to handle it. + # Sanity check. + # Note that we now support recursive types via the PREP_SESSION_ATTR, + # so we theoretically shouldn't run into this this. if recursion_level > MAX_RECURSION: raise RuntimeError('Max recursion exceeded.') @@ -108,6 +137,18 @@ class PrepSession: if not isinstance(cls, type) or not dataclasses.is_dataclass(cls): raise TypeError(f'Passed arg {cls} is not a dataclass type.') + # Add a pointer to the prep-session while doing the prep. + # This way we can ignore types that we're already in the process + # of prepping and can support recursive types. + existing_prep = getattr(cls, PREP_SESSION_ATTR, None) + if existing_prep is not None: + if existing_prep is self: + return None + # We shouldn't need to support failed preps + # or preps from multiple threads at once. + raise RuntimeError('Found existing in-progress prep.') + setattr(cls, PREP_SESSION_ATTR, self) + # Generate a warning on non-explicit preps; we prefer prep to # happen explicitly at runtime so errors can be detected early on. if not self.explicit: @@ -126,7 +167,6 @@ class PrepSession: include_extras=True) # pylint: enable=unexpected-keyword-arg except Exception as exc: - print('GOT', cls.__dict__) raise TypeError( f'dataclassio prep for {cls} failed with error: {exc}.' f' Make sure all types used in annotations are defined' @@ -175,6 +215,10 @@ class PrepSession: annotations=resolved_annotations, storage_names_to_attr_names=storage_names_to_attr_names) setattr(cls, PREP_ATTR, prepdata) + + # Clear our prep-session tag. + assert getattr(cls, PREP_SESSION_ATTR, None) is self + delattr(cls, PREP_SESSION_ATTR) return prepdata def prep_type(self, cls: type, attrname: str, anntype: Any, diff --git a/tools/efro/util.py b/tools/efro/util.py index c4c54c2e..14d8bc13 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -11,6 +11,15 @@ import functools from enum import Enum from typing import TYPE_CHECKING, cast, TypeVar, Generic +_pytz_utc: Any + +# We don't *require* pytz, but we want to support it for tzinfos if available. +try: + import pytz + _pytz_utc = pytz.utc +except ModuleNotFoundError: + _pytz_utc = None # pylint: disable=invalid-name + if TYPE_CHECKING: import asyncio from efro.call import Call as Call # 'as Call' so we re-export. @@ -62,6 +71,14 @@ def enum_by_value(cls: type[TENUM], value: Any) -> TENUM: (value, cls.__name__)) from None +def check_utc(value: datetime.datetime) -> None: + """Ensure a datetime value is timezone-aware utc.""" + if (value.tzinfo is not datetime.timezone.utc + and (_pytz_utc is None or value.tzinfo is not _pytz_utc)): + raise ValueError('datetime value does not have timezone set as' + ' datetime.timezone.utc') + + def utc_now() -> datetime.datetime: """Get offset-aware current utc time. @@ -240,7 +257,8 @@ class DispatchMethodWrapper(Generic[TARG, TRET]): pass @staticmethod - def register(func: Callable[[Any, Any], TRET]) -> Callable: + def register( + func: Callable[[Any, Any], TRET]) -> Callable[[Any, Any], TRET]: """Register a new dispatch handler for this dispatch-method.""" registry: dict[Any, Callable] @@ -312,12 +330,16 @@ class ValueDispatcher(Generic[TVAL, TRET]): return handler() return self._base_call(value) - def _add_handler(self, value: TVAL, call: Callable[[], TRET]) -> None: + def _add_handler(self, value: TVAL, + call: Callable[[], TRET]) -> Callable[[], TRET]: if value in self._handlers: raise RuntimeError(f'Duplicate handlers added for {value}') self._handlers[value] = call + return call - def register(self, value: TVAL) -> Callable[[Callable[[], TRET]], None]: + def register( + self, + value: TVAL) -> Callable[[Callable[[], TRET]], Callable[[], TRET]]: """Add a handler to the dispatcher.""" from functools import partial return partial(self._add_handler, value) @@ -343,13 +365,16 @@ class ValueDispatcher1Arg(Generic[TVAL, TARG, TRET]): return handler(arg) return self._base_call(value, arg) - def _add_handler(self, value: TVAL, call: Callable[[TARG], TRET]) -> None: + def _add_handler(self, value: TVAL, + call: Callable[[TARG], TRET]) -> Callable[[TARG], TRET]: if value in self._handlers: raise RuntimeError(f'Duplicate handlers added for {value}') self._handlers[value] = call + return call - def register(self, - value: TVAL) -> Callable[[Callable[[TARG], TRET]], None]: + def register( + self, value: TVAL + ) -> Callable[[Callable[[TARG], TRET]], Callable[[TARG], TRET]]: """Add a handler to the dispatcher.""" from functools import partial return partial(self._add_handler, value) @@ -363,8 +388,9 @@ if TYPE_CHECKING: def __call__(self, value: TVAL) -> TRET: ... - def register(self, - value: TVAL) -> Callable[[Callable[[TSELF], TRET]], None]: + def register( + self, value: TVAL + ) -> Callable[[Callable[[TSELF], TRET]], Callable[[TSELF], TRET]]: """Add a handler to the dispatcher.""" ... diff --git a/tools/efrotools/pylintplugins.py b/tools/efrotools/pylintplugins.py index 60cb5bb4..1193d047 100644 --- a/tools/efrotools/pylintplugins.py +++ b/tools/efrotools/pylintplugins.py @@ -181,8 +181,8 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: for dec in fnode.decorators.nodes: # Look for dataclassio.ioprepped. - if (isinstance(dec, astroid.nodes.Attribute) - and dec.attrname == 'ioprepped' + if (isinstance(dec, astroid.nodes.Attribute) and + dec.attrname in {'ioprepped', 'will_ioprep'} and isinstance(dec.expr, astroid.nodes.Name) and dec.expr.name == 'dataclassio'): found_ioprepped = True @@ -190,7 +190,7 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: # Look for simply 'ioprepped'. if (isinstance(dec, astroid.nodes.Name) - and dec.name == 'ioprepped'): + and dec.name in {'ioprepped', 'will_ioprep'}): found_ioprepped = True break