dycw-utilities 0.109.2__tar.gz → 0.109.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/PKG-INFO +1 -1
  2. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/pyproject.toml +2 -2
  3. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_parse.py +90 -17
  4. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_typing.py +44 -5
  5. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/__init__.py +1 -1
  6. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/parse.py +77 -22
  7. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/typing.py +9 -0
  8. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/.gitignore +0 -0
  9. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/LICENSE +0 -0
  10. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/README.md +0 -0
  11. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/__init__.py +0 -0
  12. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/conftest.py +0 -0
  13. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/__init__.py +0 -0
  14. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_missing/__init__.py +0 -0
  15. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_missing/module.py +0 -0
  16. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/__init__.py +0 -0
  17. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/outer_1.py +0 -0
  18. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/outer_2.py +0 -0
  19. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  20. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  21. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  22. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  23. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_without/__init__.py +0 -0
  24. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_without/module_1.py +0 -0
  25. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/package_without/module_2.py +0 -0
  26. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/standalone.py +0 -0
  27. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/modules/with_imports.py +0 -0
  28. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  29. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  30. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  31. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  32. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  33. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  34. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  35. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  36. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/__init__.py +0 -0
  37. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_async_service/__init__.py +0 -0
  38. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_async_service/__main__.py +0 -0
  39. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_async_service/run.sh +0 -0
  40. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_queue_processor/__init__.py +0 -0
  41. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_queue_processor/__main__.py +0 -0
  42. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/scripts/test_queue_processor/run.sh +0 -0
  43. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_altair.py +0 -0
  44. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_astor.py +0 -0
  45. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_asyncio.py +0 -0
  46. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_atomicwrites.py +0 -0
  47. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_atools.py +0 -0
  48. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_cachetools.py +0 -0
  49. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_click.py +0 -0
  50. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_concurrent.py +0 -0
  51. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_contextlib.py +0 -0
  52. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_contextvars.py +0 -0
  53. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_cryptography.py +0 -0
  54. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_cvxpy.py +0 -0
  55. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_dataclasses.py +0 -0
  56. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_datetime.py +0 -0
  57. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_enum.py +0 -0
  58. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_errors.py +0 -0
  59. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_eventkit.py +0 -0
  60. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_fastapi.py +0 -0
  61. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_fpdf2.py +0 -0
  62. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_functions.py +0 -0
  63. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_functools.py +0 -0
  64. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_getpass.py +0 -0
  65. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_git.py +0 -0
  66. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_hashlib.py +0 -0
  67. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_http.py +0 -0
  68. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_hypothesis.py +0 -0
  69. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_ipython.py +0 -0
  70. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_iterables.py +0 -0
  71. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_jupyter.py +0 -0
  72. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_logging.py +0 -0
  73. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_loguru.py +0 -0
  74. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_luigi.py +0 -0
  75. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_math.py +0 -0
  76. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_memory_profiler.py +0 -0
  77. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_modules.py +0 -0
  78. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_more_itertools.py +0 -0
  79. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_numpy.py +0 -0
  80. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_operator.py +0 -0
  81. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_optuna.py +0 -0
  82. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_orjson.py +0 -0
  83. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_os.py +0 -0
  84. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pathlib.py +0 -0
  85. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_period.py +0 -0
  86. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pickle.py +0 -0
  87. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_platform.py +0 -0
  88. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_polars.py +0 -0
  89. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pqdm.py +0 -0
  90. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pydantic.py +0 -0
  91. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pyinstrument.py +0 -0
  92. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pyrsistent.py +0 -0
  93. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pytest.py +0 -0
  94. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_pytest_regressions.py +0 -0
  95. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_python_dotenv.py +0 -0
  96. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_random.py +0 -0
  97. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_re.py +0 -0
  98. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_redis.py +0 -0
  99. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_reprlib.py +0 -0
  100. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_rich.py +0 -0
  101. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_scipy.py +0 -0
  102. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_sentinel.py +0 -0
  103. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_shelve.py +0 -0
  104. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_slack_sdk.py +0 -0
  105. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_socket.py +0 -0
  106. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_sqlalchemy.py +0 -0
  107. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_sqlalchemy_polars.py +0 -0
  108. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_streamlit.py +0 -0
  109. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_sys.py +0 -0
  110. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_tempfile.py +0 -0
  111. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_tenacity.py +0 -0
  112. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_text.py +0 -0
  113. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_threading.py +0 -0
  114. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_timer.py +0 -0
  115. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback.py +0 -0
  116. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/__init__.py +0 -0
  117. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/chain.py +0 -0
  118. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/decorated_async.py +0 -0
  119. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/decorated_sync.py +0 -0
  120. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/error_bind.py +0 -0
  121. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/many.py +0 -0
  122. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/one.py +0 -0
  123. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/recursive.py +0 -0
  124. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/task_group_one.py +0 -0
  125. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/task_group_two.py +0 -0
  126. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/two.py +0 -0
  127. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_traceback_funcs/untraced.py +0 -0
  128. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_types.py +0 -0
  129. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_typing_funcs/__init__.py +0 -0
  130. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_typing_funcs/no_future.py +0 -0
  131. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_typing_funcs/with_future.py +0 -0
  132. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_tzdata.py +0 -0
  133. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_tzlocal.py +0 -0
  134. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_uuid.py +0 -0
  135. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_version.py +0 -0
  136. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_warnings.py +0 -0
  137. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_whenever.py +0 -0
  138. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_zipfile.py +0 -0
  139. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/tests/test_zoneinfo.py +0 -0
  140. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/altair.py +0 -0
  141. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/astor.py +0 -0
  142. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/asyncio.py +0 -0
  143. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/atomicwrites.py +0 -0
  144. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/atools.py +0 -0
  145. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/cachetools.py +0 -0
  146. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/click.py +0 -0
  147. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/concurrent.py +0 -0
  148. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/contextlib.py +0 -0
  149. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/contextvars.py +0 -0
  150. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/cryptography.py +0 -0
  151. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/cvxpy.py +0 -0
  152. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/dataclasses.py +0 -0
  153. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/datetime.py +0 -0
  154. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/enum.py +0 -0
  155. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/errors.py +0 -0
  156. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/eventkit.py +0 -0
  157. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/fastapi.py +0 -0
  158. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/fpdf2.py +0 -0
  159. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/functions.py +0 -0
  160. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/functools.py +0 -0
  161. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/getpass.py +0 -0
  162. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/git.py +0 -0
  163. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/hashlib.py +0 -0
  164. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/http.py +0 -0
  165. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/hypothesis.py +0 -0
  166. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/ipython.py +0 -0
  167. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/iterables.py +0 -0
  168. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/jupyter.py +0 -0
  169. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/logging.py +0 -0
  170. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/loguru.py +0 -0
  171. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/luigi.py +0 -0
  172. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/math.py +0 -0
  173. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/memory_profiler.py +0 -0
  174. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/modules.py +0 -0
  175. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/more_itertools.py +0 -0
  176. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/numpy.py +0 -0
  177. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/operator.py +0 -0
  178. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/optuna.py +0 -0
  179. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/orjson.py +0 -0
  180. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/os.py +0 -0
  181. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pathlib.py +0 -0
  182. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/period.py +0 -0
  183. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pickle.py +0 -0
  184. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/platform.py +0 -0
  185. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/polars.py +0 -0
  186. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pqdm.py +0 -0
  187. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/py.typed +0 -0
  188. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pydantic.py +0 -0
  189. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pyinstrument.py +0 -0
  190. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pyrsistent.py +0 -0
  191. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pytest.py +0 -0
  192. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/pytest_regressions.py +0 -0
  193. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/python_dotenv.py +0 -0
  194. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/random.py +0 -0
  195. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/re.py +0 -0
  196. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/redis.py +0 -0
  197. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/reprlib.py +0 -0
  198. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/rich.py +0 -0
  199. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/scipy.py +0 -0
  200. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/sentinel.py +0 -0
  201. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/shelve.py +0 -0
  202. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/slack_sdk.py +0 -0
  203. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/socket.py +0 -0
  204. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/sqlalchemy.py +0 -0
  205. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/sqlalchemy_polars.py +0 -0
  206. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/streamlit.py +0 -0
  207. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/sys.py +0 -0
  208. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/tempfile.py +0 -0
  209. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/tenacity.py +0 -0
  210. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/text.py +0 -0
  211. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/threading.py +0 -0
  212. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/timer.py +0 -0
  213. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/traceback.py +0 -0
  214. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/types.py +0 -0
  215. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/tzdata.py +0 -0
  216. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/tzlocal.py +0 -0
  217. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/uuid.py +0 -0
  218. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/version.py +0 -0
  219. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/warnings.py +0 -0
  220. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/whenever.py +0 -0
  221. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/zipfile.py +0 -0
  222. {dycw_utilities-0.109.2 → dycw_utilities-0.109.4}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.109.2
3
+ Version: 0.109.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -89,7 +89,7 @@ dependencies = [
89
89
  name = "dycw-utilities"
90
90
  readme = "README.md"
91
91
  requires-python = ">= 3.12"
92
- version = "0.109.2"
92
+ version = "0.109.4"
93
93
 
94
94
  [project.optional-dependencies]
95
95
  test = [
@@ -332,7 +332,7 @@ zzz-test-zoneinfo = [
332
332
  # bump-my-version
333
333
  [tool.bumpversion]
334
334
  allow_dirty = true
335
- current_version = "0.109.2"
335
+ current_version = "0.109.4"
336
336
 
337
337
  [[tool.bumpversion.files]]
338
338
  filename = "src/utilities/__init__.py"
@@ -13,6 +13,7 @@ from pytest import raises
13
13
 
14
14
  from tests.test_operator import TruthEnum
15
15
  from tests.test_typing_funcs.with_future import (
16
+ DataClassFutureInt,
16
17
  TrueOrFalseFutureLit,
17
18
  TrueOrFalseFutureTypeLit,
18
19
  )
@@ -26,7 +27,11 @@ from utilities.hypothesis import (
26
27
  zoned_datetimes,
27
28
  )
28
29
  from utilities.math import is_equal
29
- from utilities.parse import ParseTextError, parse_text
30
+ from utilities.parse import (
31
+ _ParseTextExtraNonUniqueError,
32
+ _ParseTextParseError,
33
+ parse_text,
34
+ )
30
35
  from utilities.sentinel import Sentinel, sentinel
31
36
  from utilities.version import Version
32
37
  from utilities.whenever import (
@@ -62,6 +67,17 @@ class TestParseText:
62
67
  result = parse_text(TruthEnum, text)
63
68
  assert result is truth
64
69
 
70
+ @given(value=integers())
71
+ def test_extra(self, *, value: int) -> None:
72
+ text = str(value)
73
+ result = parse_text(
74
+ DataClassFutureInt,
75
+ text,
76
+ extra={DataClassFutureInt: lambda text: DataClassFutureInt(int_=int(text))},
77
+ )
78
+ expected = DataClassFutureInt(int_=value)
79
+ assert result == expected
80
+
65
81
  @given(value=floats())
66
82
  def test_float(self, *, value: float) -> None:
67
83
  text = str(value)
@@ -135,6 +151,12 @@ class TestParseText:
135
151
  result = parse_text(dt.timedelta, text)
136
152
  assert result == timedelta
137
153
 
154
+ @given(x=integers(), y=integers())
155
+ def test_tuple(self, *, x: int, y: int) -> None:
156
+ text = f"({x}, {y})"
157
+ result = parse_text(tuple[int, int], text)
158
+ assert result == (x, y)
159
+
138
160
  @given(truth=sampled_from(["true", "false"]))
139
161
  def test_type_literal(self, *, truth: Literal["true", "false"]) -> None:
140
162
  result = parse_text(TrueOrFalseFutureTypeLit, truth)
@@ -148,88 +170,139 @@ class TestParseText:
148
170
 
149
171
  def test_error_bool(self) -> None:
150
172
  with raises(
151
- ParseTextError, match="Unable to parse <class 'bool'>; got 'invalid'"
173
+ _ParseTextParseError, match="Unable to parse <class 'bool'>; got 'invalid'"
152
174
  ):
153
175
  _ = parse_text(bool, "invalid")
154
176
 
155
177
  def test_error_date(self) -> None:
156
178
  with raises(
157
- ParseTextError,
179
+ _ParseTextParseError,
158
180
  match=r"Unable to parse <class 'datetime\.date'>; got 'invalid'",
159
181
  ):
160
182
  _ = parse_text(dt.date, "invalid")
161
183
 
162
184
  def test_error_datetime(self) -> None:
163
185
  with raises(
164
- ParseTextError,
186
+ _ParseTextParseError,
165
187
  match=r"Unable to parse <class 'datetime\.datetime'>; got 'invalid'",
166
188
  ):
167
189
  _ = parse_text(dt.datetime, "invalid")
168
190
 
169
191
  def test_error_enum(self) -> None:
170
192
  with raises(
171
- ParseTextError, match="Unable to parse <enum 'TruthEnum'>; got 'invalid'"
193
+ _ParseTextParseError,
194
+ match="Unable to parse <enum 'TruthEnum'>; got 'invalid'",
172
195
  ):
173
196
  _ = parse_text(TruthEnum, "invalid")
174
197
 
198
+ def test_error_extra_empty(self) -> None:
199
+ with raises(
200
+ _ParseTextParseError,
201
+ match="Unable to parse <class 'tests.test_typing_funcs.with_future.DataClassFutureInt'>; got 'invalid'",
202
+ ):
203
+ _ = parse_text(DataClassFutureInt, "invalid", extra={})
204
+
205
+ @given(value=integers())
206
+ def test_error_extra_non_unique(self, *, value: int) -> None:
207
+ @dataclass(kw_only=True)
208
+ class Parent1:
209
+ x: int = 0
210
+
211
+ @dataclass(kw_only=True)
212
+ class Parent2:
213
+ y: int = 0
214
+
215
+ @dataclass(kw_only=True)
216
+ class Child(Parent1, Parent2): ...
217
+
218
+ with raises(
219
+ _ParseTextExtraNonUniqueError,
220
+ match="Unable to parse <class '.*'> since `extra` must contain exactly one parent class; got <function .*>, <function .*> and perhaps more",
221
+ ):
222
+ _ = parse_text(
223
+ Child,
224
+ str(value),
225
+ extra={
226
+ Parent1: lambda text: Child(x=int(text)),
227
+ Parent2: lambda text: Child(y=int(text)),
228
+ },
229
+ )
230
+
175
231
  def test_error_float(self) -> None:
176
232
  with raises(
177
- ParseTextError, match="Unable to parse <class 'float'>; got 'invalid'"
233
+ _ParseTextParseError, match="Unable to parse <class 'float'>; got 'invalid'"
178
234
  ):
179
235
  _ = parse_text(float, "invalid")
180
236
 
181
237
  def test_error_int(self) -> None:
182
238
  with raises(
183
- ParseTextError, match="Unable to parse <class 'int'>; got 'invalid'"
239
+ _ParseTextParseError, match="Unable to parse <class 'int'>; got 'invalid'"
184
240
  ):
185
241
  _ = parse_text(int, "invalid")
186
242
 
187
243
  def test_error_none(self) -> None:
188
- with raises(ParseTextError, match="Unable to parse None; got 'invalid'"):
244
+ with raises(_ParseTextParseError, match="Unable to parse None; got 'invalid'"):
189
245
  _ = parse_text(None, "invalid")
190
246
 
191
247
  def test_error_none_type(self) -> None:
192
248
  with raises(
193
- ParseTextError, match="Unable to parse <class 'NoneType'>; got 'invalid'"
249
+ _ParseTextParseError,
250
+ match="Unable to parse <class 'NoneType'>; got 'invalid'",
194
251
  ):
195
252
  _ = parse_text(NoneType, "invalid")
196
253
 
197
254
  def test_error_nullable_int(self) -> None:
198
255
  with raises(
199
- ParseTextError, match=r"Unable to parse int \| None; got 'invalid'"
256
+ _ParseTextParseError, match=r"Unable to parse int \| None; got 'invalid'"
200
257
  ):
201
258
  _ = parse_text(int | None, "invalid")
202
259
 
203
260
  def test_error_nullable_not_type(self) -> None:
204
261
  with raises(
205
- ParseTextError,
262
+ _ParseTextParseError,
206
263
  match=r"Unable to parse collections\.abc\.Iterable\[None\] \| None; got 'invalid'",
207
264
  ):
208
265
  _ = parse_text(Iterable[None] | None, "invalid")
209
266
 
210
267
  def test_error_sentinel(self) -> None:
211
268
  with raises(
212
- ParseTextError,
269
+ _ParseTextParseError,
213
270
  match=r"Unable to parse <class 'utilities\.sentinel\.Sentinel'>; got 'invalid'",
214
271
  ):
215
272
  _ = parse_text(Sentinel, "invalid")
216
273
 
217
274
  def test_error_time(self) -> None:
218
275
  with raises(
219
- ParseTextError,
276
+ _ParseTextParseError,
220
277
  match=r"Unable to parse <class 'datetime\.time'>; got 'invalid'",
221
278
  ):
222
279
  _ = parse_text(dt.time, "invalid")
223
280
 
224
281
  def test_error_timedelta(self) -> None:
225
282
  with raises(
226
- ParseTextError,
283
+ _ParseTextParseError,
227
284
  match=r"Unable to parse <class 'datetime\.timedelta'>; got 'invalid'",
228
285
  ):
229
286
  _ = parse_text(dt.timedelta, "invalid")
230
287
 
288
+ def test_error_tuple_invalid_text(self) -> None:
289
+ with raises(
290
+ _ParseTextParseError,
291
+ match=r"Unable to parse tuple\[int, int\]; got 'invalid'",
292
+ ):
293
+ _ = parse_text(tuple[int, int], "invalid")
294
+
295
+ def test_error_tuple_inconsistent_args_and_texts(self) -> None:
296
+ with raises(
297
+ _ParseTextParseError,
298
+ match=r"Unable to parse tuple\[int, int\]; got '\(text1, text2, text3\)'",
299
+ ):
300
+ _ = parse_text(tuple[int, int], "(text1, text2, text3)")
301
+
231
302
  def test_error_unknown_annotation(self) -> None:
232
- with raises(ParseTextError, match=r"Unable to parse int \| str; got 'invalid'"):
303
+ with raises(
304
+ _ParseTextParseError, match=r"Unable to parse int \| str; got 'invalid'"
305
+ ):
233
306
  _ = parse_text(int | str, "invalid")
234
307
 
235
308
  def test_error_unknown_type(self) -> None:
@@ -238,14 +311,14 @@ class TestParseText:
238
311
  pass
239
312
 
240
313
  with raises(
241
- ParseTextError,
314
+ _ParseTextParseError,
242
315
  match=r"Unable to parse <class 'tests\.test_parse\.TestParseText\.test_error_unknown_type\.<locals>\.Example'>; got 'invalid'",
243
316
  ):
244
317
  _ = parse_text(Example, "invalid")
245
318
 
246
319
  def test_error_version(self) -> None:
247
320
  with raises(
248
- ParseTextError,
321
+ _ParseTextParseError,
249
322
  match=r"Unable to parse <class 'utilities\.version\.Version'>; got 'invalid'",
250
323
  ):
251
324
  _ = parse_text(Version, "invalid")
@@ -55,6 +55,7 @@ from utilities.typing import (
55
55
  is_optional_type,
56
56
  is_sequence_type,
57
57
  is_set_type,
58
+ is_tuple_type,
58
59
  is_union_type,
59
60
  )
60
61
 
@@ -338,25 +339,63 @@ class TestIsAnnotationOfType:
338
339
  @mark.parametrize(
339
340
  ("func", "obj", "expected"),
340
341
  [
342
+ param(is_dict_type, Mapping[int, int], False),
343
+ param(is_dict_type, Sequence[int], False),
341
344
  param(is_dict_type, dict[int, int], True),
345
+ param(is_dict_type, frozenset[int], False),
342
346
  param(is_dict_type, list[int], False),
347
+ param(is_dict_type, set[int], False),
348
+ param(is_dict_type, tuple[int, int], False),
349
+ param(is_frozenset_type, Mapping[int, int], False),
350
+ param(is_frozenset_type, Sequence[int], False),
351
+ param(is_frozenset_type, dict[int, int], False),
343
352
  param(is_frozenset_type, frozenset[int], True),
344
353
  param(is_frozenset_type, list[int], False),
354
+ param(is_frozenset_type, set[int], False),
355
+ param(is_frozenset_type, tuple[int, int], False),
356
+ param(is_list_type, Mapping[int, int], False),
357
+ param(is_list_type, Sequence[int], False),
358
+ param(is_list_type, dict[int, int], False),
359
+ param(is_list_type, frozenset[int], False),
345
360
  param(is_list_type, list[int], True),
346
361
  param(is_list_type, set[int], False),
347
- param(is_mapping_type, Mapping[int, int], True),
348
- param(is_mapping_type, list[int], False),
362
+ param(is_list_type, tuple[int, int], False),
349
363
  param(is_literal_type, Literal["a", "b", "c"], True),
350
364
  param(is_literal_type, list[int], False),
365
+ param(is_mapping_type, Mapping[int, int], True),
366
+ param(is_mapping_type, Sequence[int], False),
367
+ param(is_mapping_type, dict[int, int], False),
368
+ param(is_mapping_type, frozenset[int], False),
369
+ param(is_mapping_type, list[int], False),
370
+ param(is_mapping_type, set[int], False),
371
+ param(is_mapping_type, tuple[int, int], False),
372
+ param(is_optional_type, Literal["a", "b", "c"] | None, True),
373
+ param(is_optional_type, Literal["a", "b", "c"], False),
351
374
  param(is_optional_type, int | None, True),
352
375
  param(is_optional_type, int | str, False),
353
- param(is_optional_type, list[int], False),
354
376
  param(is_optional_type, list[int] | None, True),
355
- param(is_optional_type, Literal["a", "b", "c"], False),
356
- param(is_optional_type, Literal["a", "b", "c"] | None, True),
377
+ param(is_optional_type, list[int], False),
378
+ param(is_sequence_type, Mapping[int, int], False),
357
379
  param(is_sequence_type, Sequence[int], True),
380
+ param(is_sequence_type, dict[int, int], False),
381
+ param(is_sequence_type, frozenset[int], False),
358
382
  param(is_sequence_type, list[int], False),
383
+ param(is_sequence_type, set[int], False),
384
+ param(is_sequence_type, tuple[int, int], False),
385
+ param(is_set_type, Mapping[int, int], False),
386
+ param(is_set_type, Sequence[int], False),
387
+ param(is_set_type, dict[int, int], False),
388
+ param(is_set_type, frozenset[int], False),
359
389
  param(is_set_type, list[int], False),
390
+ param(is_set_type, set[int], True),
391
+ param(is_set_type, tuple[int, int], False),
392
+ param(is_tuple_type, Mapping[int, int], False),
393
+ param(is_tuple_type, Sequence[int], False),
394
+ param(is_tuple_type, dict[int, int], False),
395
+ param(is_tuple_type, frozenset[int], False),
396
+ param(is_tuple_type, list[int], False),
397
+ param(is_tuple_type, set[int], False),
398
+ param(is_tuple_type, tuple[int, int], True),
360
399
  param(is_union_type, int | str, True),
361
400
  param(is_union_type, list[int], False),
362
401
  ],
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.109.2"
3
+ __version__ = "0.109.4"
@@ -5,30 +5,44 @@ from contextlib import suppress
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
+ from re import DOTALL
8
9
  from types import NoneType
9
- from typing import Any, override
10
+ from typing import TYPE_CHECKING, Any, TypeVar, override
10
11
 
11
12
  from utilities.datetime import is_subclass_date_not_datetime
12
13
  from utilities.enum import ParseEnumError, parse_enum
13
14
  from utilities.functions import is_subclass_int_not_bool
14
- from utilities.iterables import one, one_str
15
+ from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
16
+ from utilities.re import ExtractGroupError, extract_group
15
17
  from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
16
18
  from utilities.text import ParseBoolError, ParseNoneError, parse_bool, parse_none
17
- from utilities.typing import get_args, is_literal_type, is_optional_type
19
+ from utilities.typing import get_args, is_literal_type, is_optional_type, is_tuple_type
18
20
  from utilities.version import ParseVersionError, Version, parse_version
19
21
 
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Callable, Mapping
24
+
25
+
26
+ _T = TypeVar("_T")
27
+
20
28
 
21
29
  def parse_text(
22
- obj: Any, text: str, /, *, case_sensitive: bool = False, head: bool = False
30
+ obj: Any,
31
+ text: str,
32
+ /,
33
+ *,
34
+ case_sensitive: bool = False,
35
+ head: bool = False,
36
+ extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
23
37
  ) -> Any:
24
38
  """Parse text."""
25
39
  if obj is None:
26
40
  try:
27
41
  return parse_none(text)
28
42
  except ParseNoneError:
29
- raise ParseTextError(obj=obj, text=text) from None
43
+ raise _ParseTextParseError(obj=obj, text=text) from None
30
44
  if isinstance(obj, type):
31
- return _parse_text_type(obj, text, case_sensitive=case_sensitive)
45
+ return _parse_text_type(obj, text, case_sensitive=case_sensitive, extra=extra)
32
46
  if is_literal_type(obj):
33
47
  return one_str(get_args(obj), text, head=head, case_sensitive=case_sensitive)
34
48
  if is_optional_type(obj):
@@ -40,83 +54,111 @@ def parse_text(
40
54
  ):
41
55
  try:
42
56
  return _parse_text_type(inner, text, case_sensitive=case_sensitive)
43
- except ParseTextError:
44
- raise ParseTextError(obj=obj, text=text) from None
45
- raise ParseTextError(obj=obj, text=text) from None
57
+ except _ParseTextParseError:
58
+ raise _ParseTextParseError(obj=obj, text=text) from None
59
+ if is_tuple_type(obj):
60
+ args = get_args(obj)
61
+ try:
62
+ texts = extract_group(r"^\((.*)\)$", text, flags=DOTALL).split(", ")
63
+ except ExtractGroupError:
64
+ raise _ParseTextParseError(obj=obj, text=text) from None
65
+ if len(args) != len(texts):
66
+ raise _ParseTextParseError(obj=obj, text=text)
67
+ return tuple(
68
+ parse_text(arg, text, case_sensitive=case_sensitive, head=head)
69
+ for arg, text in zip(args, texts, strict=True)
70
+ )
71
+ raise _ParseTextParseError(obj=obj, text=text) from None
46
72
 
47
73
 
48
74
  def _parse_text_type(
49
- cls: type[Any], text: str, /, *, case_sensitive: bool = False
75
+ cls: type[Any],
76
+ text: str,
77
+ /,
78
+ *,
79
+ case_sensitive: bool = False,
80
+ extra: Mapping[type[_T], Callable[[str], _T]] | None = None,
50
81
  ) -> Any:
51
82
  """Parse text."""
52
83
  if issubclass(cls, NoneType):
53
84
  try:
54
85
  return parse_none(text)
55
86
  except ParseNoneError:
56
- raise ParseTextError(obj=cls, text=text) from None
87
+ raise _ParseTextParseError(obj=cls, text=text) from None
57
88
  if issubclass(cls, str):
58
89
  return text
59
90
  if issubclass(cls, bool):
60
91
  try:
61
92
  return parse_bool(text)
62
93
  except ParseBoolError:
63
- raise ParseTextError(obj=cls, text=text) from None
94
+ raise _ParseTextParseError(obj=cls, text=text) from None
64
95
  if is_subclass_int_not_bool(cls):
65
96
  try:
66
97
  return int(text)
67
98
  except ValueError:
68
- raise ParseTextError(obj=cls, text=text) from None
99
+ raise _ParseTextParseError(obj=cls, text=text) from None
69
100
  if issubclass(cls, float):
70
101
  try:
71
102
  return float(text)
72
103
  except ValueError:
73
- raise ParseTextError(obj=cls, text=text) from None
104
+ raise _ParseTextParseError(obj=cls, text=text) from None
74
105
  if issubclass(cls, Enum):
75
106
  try:
76
107
  return parse_enum(text, cls, case_sensitive=case_sensitive)
77
108
  except ParseEnumError:
78
- raise ParseTextError(obj=cls, text=text) from None
109
+ raise _ParseTextParseError(obj=cls, text=text) from None
79
110
  if issubclass(cls, Path):
80
111
  return Path(text).expanduser()
81
112
  if issubclass(cls, Sentinel):
82
113
  try:
83
114
  return parse_sentinel(text)
84
115
  except ParseSentinelError:
85
- raise ParseTextError(obj=cls, text=text) from None
116
+ raise _ParseTextParseError(obj=cls, text=text) from None
86
117
  if issubclass(cls, Version):
87
118
  try:
88
119
  return parse_version(text)
89
120
  except ParseVersionError:
90
- raise ParseTextError(obj=cls, text=text) from None
121
+ raise _ParseTextParseError(obj=cls, text=text) from None
91
122
  if is_subclass_date_not_datetime(cls):
92
123
  from utilities.whenever import ParseDateError, parse_date
93
124
 
94
125
  try:
95
126
  return parse_date(text)
96
127
  except ParseDateError:
97
- raise ParseTextError(obj=cls, text=text) from None
128
+ raise _ParseTextParseError(obj=cls, text=text) from None
98
129
  if issubclass(cls, dt.datetime):
99
130
  from utilities.whenever import ParseDateTimeError, parse_datetime
100
131
 
101
132
  try:
102
133
  return parse_datetime(text)
103
134
  except ParseDateTimeError:
104
- raise ParseTextError(obj=cls, text=text) from None
135
+ raise _ParseTextParseError(obj=cls, text=text) from None
105
136
  if issubclass(cls, dt.time):
106
137
  from utilities.whenever import ParseTimeError, parse_time
107
138
 
108
139
  try:
109
140
  return parse_time(text)
110
141
  except ParseTimeError:
111
- raise ParseTextError(obj=cls, text=text) from None
142
+ raise _ParseTextParseError(obj=cls, text=text) from None
112
143
  if issubclass(cls, dt.timedelta):
113
144
  from utilities.whenever import ParseTimedeltaError, parse_timedelta
114
145
 
115
146
  try:
116
147
  return parse_timedelta(text)
117
148
  except ParseTimedeltaError:
118
- raise ParseTextError(obj=cls, text=text) from None
119
- raise ParseTextError(obj=cls, text=text) from None
149
+ raise _ParseTextParseError(obj=cls, text=text) from None
150
+ if extra is not None:
151
+ try:
152
+ parser = one(p for c, p in extra.items() if issubclass(cls, c))
153
+ except OneEmptyError:
154
+ pass
155
+ except OneNonUniqueError as error:
156
+ raise _ParseTextExtraNonUniqueError(
157
+ obj=cls, text=text, first=error.first, second=error.second
158
+ ) from None
159
+ else:
160
+ return parser(text)
161
+ raise _ParseTextParseError(obj=cls, text=text) from None
120
162
 
121
163
 
122
164
  @dataclass
@@ -124,6 +166,19 @@ class ParseTextError(Exception):
124
166
  obj: Any
125
167
  text: str
126
168
 
169
+
170
+ @dataclass
171
+ class _ParseTextParseError(ParseTextError):
127
172
  @override
128
173
  def __str__(self) -> str:
129
174
  return f"Unable to parse {self.obj!r}; got {self.text!r}"
175
+
176
+
177
+ @dataclass
178
+ class _ParseTextExtraNonUniqueError(ParseTextError):
179
+ first: type[Any]
180
+ second: type[Any]
181
+
182
+ @override
183
+ def __str__(self) -> str:
184
+ return f"Unable to parse {self.obj!r} since `extra` must contain exactly one parent class; got {self.first!r}, {self.second!r} and perhaps more"
@@ -182,6 +182,14 @@ def is_set_type(obj: Any, /) -> bool:
182
182
  ##
183
183
 
184
184
 
185
+ def is_tuple_type(obj: Any, /) -> bool:
186
+ """Check if an object is a tuple type annotation."""
187
+ return _is_annotation_of_type(obj, tuple)
188
+
189
+
190
+ ##
191
+
192
+
185
193
  def is_union_type(obj: Any, /) -> bool:
186
194
  """Check if an object is a union type annotation."""
187
195
  is_old_union = _is_annotation_of_type(obj, Union) # pyright: ignore[reportDeprecated]
@@ -212,5 +220,6 @@ __all__ = [
212
220
  "is_optional_type",
213
221
  "is_sequence_type",
214
222
  "is_set_type",
223
+ "is_tuple_type",
215
224
  "is_union_type",
216
225
  ]