shellsim 0.1.18__tar.gz → 0.1.19__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 (213) hide show
  1. {shellsim-0.1.18 → shellsim-0.1.19}/Cargo.lock +1 -1
  2. {shellsim-0.1.18 → shellsim-0.1.19}/Cargo.toml +1 -1
  3. {shellsim-0.1.18 → shellsim-0.1.19}/PKG-INFO +1 -1
  4. {shellsim-0.1.18 → shellsim-0.1.19}/python/native/Cargo.lock +2 -2
  5. {shellsim-0.1.18 → shellsim-0.1.19}/python/native/Cargo.toml +1 -1
  6. {shellsim-0.1.18 → shellsim-0.1.19}/src/interp.rs +12 -1
  7. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/parser.rs +20 -0
  8. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/protocol.rs +2 -2
  9. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/core.rs +33 -0
  10. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/math.rs +43 -0
  11. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/random.py +17 -0
  12. shellsim-0.1.19/src/python/vm/format.rs +352 -0
  13. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/operations.rs +30 -39
  14. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm.rs +2 -241
  15. {shellsim-0.1.18 → shellsim-0.1.19}/LICENSE +0 -0
  16. {shellsim-0.1.18 → shellsim-0.1.19}/README.md +0 -0
  17. {shellsim-0.1.18 → shellsim-0.1.19}/pyproject.toml +0 -0
  18. {shellsim-0.1.18 → shellsim-0.1.19}/python/native/src/lib.rs +0 -0
  19. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/__init__.py +0 -0
  20. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/__main__.py +0 -0
  21. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/_api.py +0 -0
  22. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/_cli.py +0 -0
  23. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/py.typed +0 -0
  24. {shellsim-0.1.18 → shellsim-0.1.19}/python/shellsim/python.py +0 -0
  25. {shellsim-0.1.18 → shellsim-0.1.19}/rust-toolchain.toml +0 -0
  26. {shellsim-0.1.18 → shellsim-0.1.19}/src/arith.rs +0 -0
  27. {shellsim-0.1.18 → shellsim-0.1.19}/src/bin/shellsim-python.rs +0 -0
  28. {shellsim-0.1.18 → shellsim-0.1.19}/src/clock.rs +0 -0
  29. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/archives.rs +0 -0
  30. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/arcmd.rs +0 -0
  31. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/ast.rs +0 -0
  32. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/eval.rs +0 -0
  33. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/lexer.rs +0 -0
  34. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk/parser.rs +0 -0
  35. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/awk.rs +0 -0
  36. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/bc.rs +0 -0
  37. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/builtins.rs +0 -0
  38. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/dd.rs +0 -0
  39. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/diff.rs +0 -0
  40. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/echo.rs +0 -0
  41. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/expr.rs +0 -0
  42. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/find.rs +0 -0
  43. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/format.rs +0 -0
  44. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/fs.rs +0 -0
  45. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/apply.rs +0 -0
  46. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/commit.rs +0 -0
  47. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/compare.rs +0 -0
  48. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/config.rs +0 -0
  49. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/conflict.rs +0 -0
  50. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/diff.rs +0 -0
  51. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/ignore.rs +0 -0
  52. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/import.rs +0 -0
  53. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/log.rs +0 -0
  54. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/merge.rs +0 -0
  55. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/mod.rs +0 -0
  56. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/plumbing.rs +0 -0
  57. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/rebase.rs +0 -0
  58. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/refs.rs +0 -0
  59. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/repo.rs +0 -0
  60. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/stash.rs +0 -0
  61. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/switch.rs +0 -0
  62. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/git/worktree.rs +0 -0
  63. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/grep.rs +0 -0
  64. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/hashing.rs +0 -0
  65. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/makecmd.rs +0 -0
  66. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/mod.rs +0 -0
  67. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/net.rs +0 -0
  68. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/options.rs +0 -0
  69. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/patch.rs +0 -0
  70. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/pkg.rs +0 -0
  71. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/printf.rs +0 -0
  72. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/proc.rs +0 -0
  73. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/regex_compat.rs +0 -0
  74. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/rgcmd.rs +0 -0
  75. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/sed.rs +0 -0
  76. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/sort.rs +0 -0
  77. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/streams.rs +0 -0
  78. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/system.rs +0 -0
  79. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/tarcmd.rs +0 -0
  80. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/text.rs +0 -0
  81. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/unavailable.rs +0 -0
  82. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/util.rs +0 -0
  83. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/wasm.rs +0 -0
  84. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/xargs.rs +0 -0
  85. {shellsim-0.1.18 → shellsim-0.1.19}/src/commands/zipcmd.rs +0 -0
  86. {shellsim-0.1.18 → shellsim-0.1.19}/src/corpus.rs +0 -0
  87. {shellsim-0.1.18 → shellsim-0.1.19}/src/descriptors.rs +0 -0
  88. {shellsim-0.1.18 → shellsim-0.1.19}/src/display.rs +0 -0
  89. {shellsim-0.1.18 → shellsim-0.1.19}/src/exec.rs +0 -0
  90. {shellsim-0.1.18 → shellsim-0.1.19}/src/expand.rs +0 -0
  91. {shellsim-0.1.18 → shellsim-0.1.19}/src/harness.rs +0 -0
  92. {shellsim-0.1.18 → shellsim-0.1.19}/src/harness_manager.rs +0 -0
  93. {shellsim-0.1.18 → shellsim-0.1.19}/src/hashes.rs +0 -0
  94. {shellsim-0.1.18 → shellsim-0.1.19}/src/host_ingest.rs +0 -0
  95. {shellsim-0.1.18 → shellsim-0.1.19}/src/jqcmd.rs +0 -0
  96. {shellsim-0.1.18 → shellsim-0.1.19}/src/lib.rs +0 -0
  97. {shellsim-0.1.18 → shellsim-0.1.19}/src/main.rs +0 -0
  98. {shellsim-0.1.18 → shellsim-0.1.19}/src/mcp.rs +0 -0
  99. {shellsim-0.1.18 → shellsim-0.1.19}/src/net.rs +0 -0
  100. {shellsim-0.1.18 → shellsim-0.1.19}/src/netcmd.rs +0 -0
  101. {shellsim-0.1.18 → shellsim-0.1.19}/src/process.rs +0 -0
  102. {shellsim-0.1.18 → shellsim-0.1.19}/src/profile.rs +0 -0
  103. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/cat.rs +0 -0
  104. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/env.rs +0 -0
  105. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/find.rs +0 -0
  106. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/head.rs +0 -0
  107. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/make.rs +0 -0
  108. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/nice.rs +0 -0
  109. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/nohup.rs +0 -0
  110. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/sleep.rs +0 -0
  111. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/tee.rs +0 -0
  112. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/timeout.rs +0 -0
  113. {shellsim-0.1.18 → shellsim-0.1.19}/src/program/xargs.rs +0 -0
  114. {shellsim-0.1.18 → shellsim-0.1.19}/src/program.rs +0 -0
  115. {shellsim-0.1.18 → shellsim-0.1.19}/src/pseudo_fs.rs +0 -0
  116. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/ast.rs +0 -0
  117. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/bytecode.rs +0 -0
  118. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/compiler.rs +0 -0
  119. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/filesystem.rs +0 -0
  120. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/heap.rs +0 -0
  121. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/http.rs +0 -0
  122. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/lexer.rs +0 -0
  123. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/mapping.rs +0 -0
  124. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/mod.rs +0 -0
  125. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/native.rs +0 -0
  126. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/number.rs +0 -0
  127. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/object_model.rs +0 -0
  128. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/process.rs +0 -0
  129. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/slice.rs +0 -0
  130. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/source.rs +0 -0
  131. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/argparse.rs +0 -0
  132. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/asyncio.rs +0 -0
  133. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/base64.rs +0 -0
  134. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/bisect.rs +0 -0
  135. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/collections.rs +0 -0
  136. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/dataclasses.rs +0 -0
  137. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/enum.rs +0 -0
  138. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/frozen.rs +0 -0
  139. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/functools.rs +0 -0
  140. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/hashlib.rs +0 -0
  141. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/heapq.rs +0 -0
  142. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/http.rs +0 -0
  143. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/importlib.rs +0 -0
  144. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/itertools.rs +0 -0
  145. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/json.rs +0 -0
  146. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/mod.rs +0 -0
  147. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/numpy.rs +0 -0
  148. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/os.rs +0 -0
  149. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/pytest.rs +0 -0
  150. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/re.rs +0 -0
  151. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/abc.py +0 -0
  152. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/asyncio.py +0 -0
  153. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/base64.py +0 -0
  154. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/cmath.py +0 -0
  155. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/codecs.py +0 -0
  156. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/collections.py +0 -0
  157. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/complex.py +0 -0
  158. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/copy.py +0 -0
  159. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/csv.py +0 -0
  160. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/datetime.py +0 -0
  161. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/functools.py +0 -0
  162. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/glob.py +0 -0
  163. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/hashlib.py +0 -0
  164. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/http.py +0 -0
  165. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/http_client.py +0 -0
  166. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/importlib.py +0 -0
  167. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/io.py +0 -0
  168. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/json.py +0 -0
  169. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/logging.py +0 -0
  170. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/numpy_random.py +0 -0
  171. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/os.py +0 -0
  172. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/pathlib.py +0 -0
  173. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/pytest.py +0 -0
  174. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/shutil.py +0 -0
  175. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/signal.py +0 -0
  176. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/statistics.py +0 -0
  177. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/struct.py +0 -0
  178. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/subprocess.py +0 -0
  179. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/tempfile.py +0 -0
  180. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/textwrap.py +0 -0
  181. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib.py +0 -0
  182. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib_error.py +0 -0
  183. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/urllib_request.py +0 -0
  184. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/uuid.py +0 -0
  185. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/zipfile.py +0 -0
  186. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/source/zlib.py +0 -0
  187. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/string.rs +0 -0
  188. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/struct.rs +0 -0
  189. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/subprocess.rs +0 -0
  190. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/sys.rs +0 -0
  191. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/time.rs +0 -0
  192. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/typing.rs +0 -0
  193. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/unittest.rs +0 -0
  194. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/vfs.rs +0 -0
  195. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/stdlib/zlib.rs +0 -0
  196. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/string.rs +0 -0
  197. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/token.rs +0 -0
  198. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/calls.rs +0 -0
  199. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/dispatch.rs +0 -0
  200. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/host.rs +0 -0
  201. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/iteration.rs +0 -0
  202. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/namespace.rs +0 -0
  203. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/native_runtime.rs +0 -0
  204. {shellsim-0.1.18 → shellsim-0.1.19}/src/python/vm/objects.rs +0 -0
  205. {shellsim-0.1.18 → shellsim-0.1.19}/src/realtime.rs +0 -0
  206. {shellsim-0.1.18 → shellsim-0.1.19}/src/resources.rs +0 -0
  207. {shellsim-0.1.18 → shellsim-0.1.19}/src/sandbox.rs +0 -0
  208. {shellsim-0.1.18 → shellsim-0.1.19}/src/scenario.rs +0 -0
  209. {shellsim-0.1.18 → shellsim-0.1.19}/src/scheduler.rs +0 -0
  210. {shellsim-0.1.18 → shellsim-0.1.19}/src/shell.rs +0 -0
  211. {shellsim-0.1.18 → shellsim-0.1.19}/src/syscalls.rs +0 -0
  212. {shellsim-0.1.18 → shellsim-0.1.19}/src/telemetry.rs +0 -0
  213. {shellsim-0.1.18 → shellsim-0.1.19}/src/vfs.rs +0 -0
@@ -861,7 +861,7 @@ dependencies = [
861
861
 
862
862
  [[package]]
863
863
  name = "shellsim"
864
- version = "0.1.18"
864
+ version = "0.1.19"
865
865
  dependencies = [
866
866
  "base64",
867
867
  "crc32fast",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "shellsim"
3
- version = "0.1.18"
3
+ version = "0.1.19"
4
4
  edition = "2021"
5
5
  description = "In-process BusyBox-like environment for RL rollouts and agent tests"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shellsim
3
- Version: 0.1.18
3
+ Version: 0.1.19
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: License :: OSI Approved :: Apache Software License
6
6
  Classifier: Operating System :: MacOS :: MacOS X
@@ -925,7 +925,7 @@ dependencies = [
925
925
 
926
926
  [[package]]
927
927
  name = "shellsim"
928
- version = "0.1.18"
928
+ version = "0.1.19"
929
929
  dependencies = [
930
930
  "base64",
931
931
  "crc32fast",
@@ -947,7 +947,7 @@ dependencies = [
947
947
 
948
948
  [[package]]
949
949
  name = "shellsim-python-bindings"
950
- version = "0.1.18"
950
+ version = "0.1.19"
951
951
  dependencies = [
952
952
  "pyo3",
953
953
  "serde",
@@ -2,7 +2,7 @@
2
2
 
3
3
  [package]
4
4
  name = "shellsim-python-bindings"
5
- version = "0.1.18"
5
+ version = "0.1.19"
6
6
  edition = "2021"
7
7
  description = "Python bindings for shellsim"
8
8
  license = "Apache-2.0"
@@ -162,7 +162,7 @@ pub struct ProcessState {
162
162
  pub session_id: ProcessId,
163
163
  /// Process group inherited across ordinary forks or established by a group leader.
164
164
  pub process_group: ProcessId,
165
- /// PID expanded by `$$`; preserved across Bash subshells.
165
+ /// PID expanded by `$$`; preserved across Bash subshells and reset when a shell image loads.
166
166
  pub shell_pid: ProcessId,
167
167
  /// shell + environment variables (we don't distinguish exported vs not for simplicity,
168
168
  /// except that `env`/child python only sees exported ones, tracked in `exported`)
@@ -1102,6 +1102,17 @@ impl Environment {
1102
1102
  state.command_hash.clear();
1103
1103
  state.directory_stack.clear();
1104
1104
  state.local_scopes.clear();
1105
+ // The new image is a fresh shell: `$$` names it, and loop, job, and getopts state from
1106
+ // the forked parent shell do not carry over.
1107
+ state.shell_pid = pid;
1108
+ state.jobs.clear();
1109
+ state.next_job_id = 1;
1110
+ state.loop_depth = 0;
1111
+ state.cond_depth = 0;
1112
+ state.getopts = GetoptsState {
1113
+ optind: 1,
1114
+ offset: 1,
1115
+ };
1105
1116
  state.arg0 = image.arg0;
1106
1117
  state.positional = image.positional;
1107
1118
  state.opt_errexit = image.errexit;
@@ -2101,6 +2101,26 @@ mod tests {
2101
2101
  parse(lex("sys.stdout.write(sys.argv[1])").unwrap()).unwrap();
2102
2102
  }
2103
2103
 
2104
+ #[test]
2105
+ fn parses_comma_separated_imports_and_rejects_a_missing_module() {
2106
+ let program = parse(lex("import math, urllib.error as errors, random").unwrap()).unwrap();
2107
+ let StatementKind::Import { modules } = &program.statements[0].kind else {
2108
+ panic!("expected import statement")
2109
+ };
2110
+ assert_eq!(
2111
+ modules,
2112
+ &[
2113
+ ("math".into(), "math".into()),
2114
+ ("urllib.error".into(), "errors".into()),
2115
+ ("random".into(), "random".into())
2116
+ ]
2117
+ );
2118
+ for source in ["import math,", "import , math"] {
2119
+ let error = parse(lex(source).unwrap()).expect_err("missing module must fail");
2120
+ assert!(error.message.contains("expected a module name"));
2121
+ }
2122
+ }
2123
+
2104
2124
  #[test]
2105
2125
  fn parses_tuple_subscripts_and_rejects_empty_tuple_members() {
2106
2126
  parse(lex("pair = values[1, 2]\nsingle = values[1,]").unwrap()).unwrap();
@@ -17,8 +17,8 @@ pub fn display(heap: &Heap, value: &Value) -> Result<String, String> {
17
17
  if let Some(value) = string_value(heap, value)? {
18
18
  return Ok(value);
19
19
  }
20
- if let Some((kind, message)) = exception_parts(heap, value)? {
21
- return Ok(if message.is_empty() { kind } else { message });
20
+ if let Some((_, message)) = exception_parts(heap, value)? {
21
+ return Ok(message);
22
22
  }
23
23
  if let Some((_, args)) = user_exception_parts(heap, value)? {
24
24
  return match args.as_slice() {
@@ -19,6 +19,7 @@ use super::super::number::PyNumber;
19
19
  use super::super::slice::SlicePlan;
20
20
 
21
21
  static BUILTINS: &[FunctionDef] = &[
22
+ builtin("__import__", builtin_import),
22
23
  builtin("map", builtin_map),
23
24
  builtin("filter", builtin_filter),
24
25
  builtin("reversed", builtin_reversed),
@@ -2899,3 +2900,35 @@ fn slot_sequence_multiply(
2899
2900
  _ => unreachable!("only concrete sequence slots call this helper"),
2900
2901
  }
2901
2902
  }
2903
+
2904
+ /// Import only through the simulated module loader, preserving dotted-import return behavior.
2905
+ fn builtin_import(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
2906
+ args.expect_positional("__import__", 1, 5)?;
2907
+ args.reject_unknown_keywords("__import__", &["globals", "locals", "fromlist", "level"])?;
2908
+ let OwnedPyString(name) = args.positional()[0].cast(runtime)?;
2909
+ let level = args
2910
+ .positional()
2911
+ .get(4)
2912
+ .copied()
2913
+ .or(args.keyword("__import__", "level")?.copied());
2914
+ if let Some(level) = level {
2915
+ if !matches!(
2916
+ integer_argument(runtime, &level, "level must be an integer")?,
2917
+ IntegerArgument::Finite(0)
2918
+ ) {
2919
+ return Err(PyError::value_error("relative __import__ is not supported"));
2920
+ }
2921
+ }
2922
+ let fromlist = args
2923
+ .positional()
2924
+ .get(3)
2925
+ .copied()
2926
+ .or(args.keyword("__import__", "fromlist")?.copied());
2927
+ let module = runtime.import_module(&name)?;
2928
+ if let Some(fromlist) = fromlist {
2929
+ if runtime.truth(&fromlist)? {
2930
+ return Ok(module);
2931
+ }
2932
+ }
2933
+ runtime.import_module(name.split('.').next().unwrap_or(&name))
2934
+ }
@@ -45,6 +45,11 @@ pub(super) static MODULE: ModuleDef = ModuleDef {
45
45
  name: "ceil",
46
46
  call: native_ceil,
47
47
  },
48
+ FunctionDef {
49
+ module: "math",
50
+ name: "comb",
51
+ call: native_comb,
52
+ },
48
53
  FunctionDef {
49
54
  module: "math",
50
55
  name: "cos",
@@ -195,6 +200,44 @@ fn native_ceil(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
195
200
  native_call(runtime, args, "ceil")
196
201
  }
197
202
 
203
+ /// Compute exact combinations with work and result storage bounded before multiplication.
204
+ fn native_comb(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
205
+ args.expect_positional("math.comb", 2, 2)?;
206
+ args.reject_keywords("math.comb")?;
207
+ let n = integer_argument(runtime, &args.positional()[0], "comb")?;
208
+ let k = integer_argument(runtime, &args.positional()[1], "comb")?;
209
+ if n.is_negative() {
210
+ return Err(PyError::value_error("n must be a non-negative integer"));
211
+ }
212
+ if k.is_negative() {
213
+ return Err(PyError::value_error("k must be a non-negative integer"));
214
+ }
215
+ if k > n {
216
+ return runtime.new_integer("0");
217
+ }
218
+ let complement = &n - &k;
219
+ let selected = if k <= complement { k } else { complement };
220
+ let count = selected
221
+ .to_usize()
222
+ .ok_or_else(|| PyError::resource_error("combination length is too large"))?;
223
+ if count > 100_000 {
224
+ return Err(PyError::resource_error("combination length is too large"));
225
+ }
226
+ if count == 0 {
227
+ return runtime.new_integer("1");
228
+ }
229
+ let bytes = bigint_bytes(&n)?
230
+ .checked_mul(count.saturating_add(1))
231
+ .ok_or_else(|| PyError::resource_error("combination result is too large"))?;
232
+ runtime.reserve_memory(bytes)?;
233
+ let mut result = BigInt::from(1_u8);
234
+ for index in 1..=count {
235
+ runtime.charge_cpu(result.bits().div_ceil(64).saturating_add(1))?;
236
+ result = result * (&n - &selected + index) / index;
237
+ }
238
+ runtime.new_integer(&result.to_string())
239
+ }
240
+
198
241
  fn native_cos(runtime: &mut dyn PyRuntime, args: CallArgs) -> PyResult {
199
242
  native_call(runtime, args, "cos")
200
243
  }
@@ -11,6 +11,7 @@ class Random:
11
11
  if not isinstance(value, int):
12
12
  raise TypeError("shellsim random seeds must be integers or None")
13
13
  self._state = value % 2147483648
14
+ self._gauss_next = None
14
15
 
15
16
  def _next(self):
16
17
  self._state = (self._state * 1103515245 + 12345) % 2147483648
@@ -40,6 +41,18 @@ class Random:
40
41
  def uniform(self, left, right):
41
42
  return left + (right - left) * self.random()
42
43
 
44
+ def gauss(self, mu=0.0, sigma=1.0):
45
+ import math
46
+
47
+ value = self._gauss_next
48
+ self._gauss_next = None
49
+ if value is None:
50
+ angle = math.tau * self.random()
51
+ radius = math.sqrt(-2.0 * math.log(1.0 - self.random()))
52
+ value = math.cos(angle) * radius
53
+ self._gauss_next = math.sin(angle) * radius
54
+ return mu + value * sigma
55
+
43
56
  def choice(self, population):
44
57
  if len(population) == 0:
45
58
  raise IndexError("cannot choose from an empty sequence")
@@ -85,6 +98,10 @@ def uniform(left, right):
85
98
  return _default.uniform(left, right)
86
99
 
87
100
 
101
+ def gauss(mu=0.0, sigma=1.0):
102
+ return _default.gauss(mu, sigma)
103
+
104
+
88
105
  def choice(population):
89
106
  return _default.choice(population)
90
107
 
@@ -0,0 +1,352 @@
1
+ //! Python's format-specification mini-language for f-strings, `format`, and `str.format`.
2
+ //!
3
+ //! A specification is parsed once into [`FormatSpec`], then rendered for an integer, float, or
4
+ //! string. The supported grammar is `[align][sign][#][0][width][,][.precision][type]` with the
5
+ //! default space fill. Custom fill characters, `=` alignment, the space sign, `_` grouping, and
6
+ //! the `n`/`c` presentations are rejected explicitly rather than approximated.
7
+
8
+ use super::BigInt;
9
+ use num_bigint::Sign;
10
+
11
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
12
+ pub(super) enum Align {
13
+ Left,
14
+ Right,
15
+ Center,
16
+ }
17
+
18
+ /// A parsed format specification. Widths and precisions are bounded by the caller's resource
19
+ /// reservation before rendering allocates padding.
20
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21
+ pub(super) struct FormatSpec {
22
+ pub(super) align: Option<Align>,
23
+ pub(super) plus: bool,
24
+ pub(super) alternate: bool,
25
+ pub(super) zero: bool,
26
+ pub(super) width: usize,
27
+ pub(super) grouping: bool,
28
+ pub(super) precision: Option<usize>,
29
+ pub(super) presentation: Option<char>,
30
+ }
31
+
32
+ impl FormatSpec {
33
+ /// Parse `spec`, for example `">+12,.2f"` or `"#010x"`.
34
+ pub(super) fn parse(spec: &str) -> Result<Self, String> {
35
+ let unsupported = || format!("unsupported format specification {spec:?}");
36
+ let mut parsed = Self::default();
37
+ let mut rest = spec;
38
+ if let Some(align) = rest.chars().next().and_then(|first| match first {
39
+ '<' => Some(Align::Left),
40
+ '>' => Some(Align::Right),
41
+ '^' => Some(Align::Center),
42
+ _ => None,
43
+ }) {
44
+ parsed.align = Some(align);
45
+ rest = &rest[1..];
46
+ }
47
+ if let Some(stripped) = rest.strip_prefix('+') {
48
+ parsed.plus = true;
49
+ rest = stripped;
50
+ } else if let Some(stripped) = rest.strip_prefix('-') {
51
+ rest = stripped;
52
+ }
53
+ if let Some(stripped) = rest.strip_prefix('#') {
54
+ parsed.alternate = true;
55
+ rest = stripped;
56
+ }
57
+ if let Some(stripped) = rest.strip_prefix('0') {
58
+ parsed.zero = true;
59
+ rest = stripped;
60
+ }
61
+ let (width, stripped) = leading_number(rest).map_err(|_| unsupported())?;
62
+ parsed.width = width.unwrap_or(0);
63
+ rest = stripped;
64
+ if let Some(stripped) = rest.strip_prefix(',') {
65
+ parsed.grouping = true;
66
+ rest = stripped;
67
+ }
68
+ if let Some(stripped) = rest.strip_prefix('.') {
69
+ let (precision, stripped) = leading_number(stripped).map_err(|_| unsupported())?;
70
+ parsed.precision = Some(precision.ok_or_else(unsupported)?);
71
+ rest = stripped;
72
+ }
73
+ let mut chars = rest.chars();
74
+ parsed.presentation = chars.next();
75
+ if chars.next().is_some() {
76
+ return Err(unsupported());
77
+ }
78
+ Ok(parsed)
79
+ }
80
+
81
+ fn sign(&self, negative: bool) -> &'static str {
82
+ if negative {
83
+ "-"
84
+ } else if self.plus {
85
+ "+"
86
+ } else {
87
+ ""
88
+ }
89
+ }
90
+
91
+ /// Pad a rendered number. Zero padding goes between the sign and the digits, and explicit
92
+ /// alignment takes precedence over it, as in CPython.
93
+ fn pad_number(&self, sign: &str, body: String) -> String {
94
+ let body = if self.grouping {
95
+ group_decimal(&body, self.zero_width(sign.len()))
96
+ } else {
97
+ body
98
+ };
99
+ let rendered = format!("{sign}{body}");
100
+ if self.align.is_none() && self.zero {
101
+ let padding = self.width.saturating_sub(rendered.len());
102
+ return format!("{sign}{}{body}", "0".repeat(padding));
103
+ }
104
+ align(&rendered, self.width, self.align.unwrap_or(Align::Right))
105
+ }
106
+
107
+ /// The minimum grouped digit-field width when zero padding applies.
108
+ fn zero_width(&self, sign: usize) -> usize {
109
+ if self.zero && self.align.is_none() {
110
+ self.width.saturating_sub(sign)
111
+ } else {
112
+ 0
113
+ }
114
+ }
115
+ }
116
+
117
+ fn leading_number(text: &str) -> Result<(Option<usize>, &str), std::num::ParseIntError> {
118
+ let digits = text.bytes().take_while(u8::is_ascii_digit).count();
119
+ if digits == 0 {
120
+ return Ok((None, text));
121
+ }
122
+ Ok((Some(text[..digits].parse()?), &text[digits..]))
123
+ }
124
+
125
+ /// Format an integer with the `d`, `b`, `o`, `x`, `X`, or default presentation.
126
+ pub(super) fn format_integer(value: BigInt, spec: &FormatSpec) -> Result<String, String> {
127
+ if spec.precision.is_some() {
128
+ return Err("precision is not allowed in integer format".into());
129
+ }
130
+ let (radix, prefix, uppercase) = match spec.presentation {
131
+ None | Some('d') => (10, "", false),
132
+ Some('b') => (2, "0b", false),
133
+ Some('o') => (8, "0o", false),
134
+ Some('x') => (16, "0x", false),
135
+ Some('X') => (16, "0X", true),
136
+ Some(other) => return Err(format!("unsupported integer format type {other:?}")),
137
+ };
138
+ if spec.alternate && radix == 10 {
139
+ return Err("alternate form is not allowed for decimal integers".into());
140
+ }
141
+ if spec.grouping && radix != 10 {
142
+ return Err("comma grouping requires decimal presentation".into());
143
+ }
144
+ let negative = value.sign() == Sign::Minus;
145
+ let magnitude = if negative { -value } else { value };
146
+ let mut digits = magnitude.to_str_radix(radix);
147
+ if uppercase {
148
+ digits.make_ascii_uppercase();
149
+ }
150
+ let prefix = if spec.alternate { prefix } else { "" };
151
+ if !prefix.is_empty() && spec.zero && spec.align.is_none() {
152
+ let content = spec.sign(negative).len() + prefix.len() + digits.len();
153
+ let padding = spec.width.saturating_sub(content);
154
+ return Ok(format!(
155
+ "{}{prefix}{}{digits}",
156
+ spec.sign(negative),
157
+ "0".repeat(padding)
158
+ ));
159
+ }
160
+ Ok(spec.pad_number(spec.sign(negative), format!("{prefix}{digits}")))
161
+ }
162
+
163
+ /// Format a float with the `f`, `e`, `E`, `g`, `G`, `%`, or default presentation. `repr` is
164
+ /// Python's shortest round-trip text, used when neither a presentation nor a precision is given.
165
+ pub(super) fn format_float(value: f64, repr: &str, spec: &FormatSpec) -> Result<String, String> {
166
+ if spec.alternate {
167
+ return Err("alternate form is not supported for floats".into());
168
+ }
169
+ let precision = spec.precision.unwrap_or(6);
170
+ let magnitude = value.abs();
171
+ let body = match spec.presentation {
172
+ _ if !value.is_finite() => non_finite(value, spec.presentation),
173
+ Some('f') => format!("{magnitude:.precision$}"),
174
+ Some('e') => scientific(magnitude, precision, 'e'),
175
+ Some('E') => scientific(magnitude, precision, 'E'),
176
+ Some('g') => format_general(magnitude, precision, false, false),
177
+ Some('G') => format_general(magnitude, precision, false, true),
178
+ Some('%') => format!("{:.precision$}%", magnitude * 100.0),
179
+ None => match spec.precision {
180
+ Some(precision) => format_general(magnitude, precision, true, false),
181
+ None => repr.trim_start_matches('-').to_string(),
182
+ },
183
+ Some(other) => return Err(format!("unsupported floating-point format type {other:?}")),
184
+ };
185
+ let body = match spec.presentation {
186
+ Some('%') if !value.is_finite() => format!("{body}%"),
187
+ _ => body,
188
+ };
189
+ Ok(spec.pad_number(spec.sign(value.is_sign_negative()), body))
190
+ }
191
+
192
+ /// Format a string with the `s` or default presentation.
193
+ pub(super) fn format_text(value: &str, spec: &FormatSpec) -> Result<String, String> {
194
+ if spec.plus || spec.alternate || spec.zero || spec.grouping || spec.precision.is_some() {
195
+ return Err("numeric format options are not allowed for strings".into());
196
+ }
197
+ if !matches!(spec.presentation, None | Some('s')) {
198
+ return Err(format!(
199
+ "unsupported string format type {:?}",
200
+ spec.presentation.unwrap_or_default()
201
+ ));
202
+ }
203
+ Ok(align(value, spec.width, spec.align.unwrap_or(Align::Left)))
204
+ }
205
+
206
+ fn non_finite(value: f64, presentation: Option<char>) -> String {
207
+ let label = if value.is_nan() { "nan" } else { "inf" };
208
+ if matches!(presentation, Some('E' | 'G')) {
209
+ label.to_uppercase()
210
+ } else {
211
+ label.into()
212
+ }
213
+ }
214
+
215
+ fn scientific(value: f64, precision: usize, marker: char) -> String {
216
+ let rendered = format!("{value:.precision$e}");
217
+ let (mantissa, exponent) = rendered.split_once('e').expect("Rust scientific format");
218
+ let exponent = exponent.parse::<i32>().expect("Rust scientific exponent");
219
+ format!("{mantissa}{marker}{exponent:+03}")
220
+ }
221
+
222
+ /// Format significant digits of a nonnegative finite value using Python's fixed/scientific
223
+ /// thresholds. The default presentation keeps a trailing `.0` and switches to scientific one
224
+ /// digit earlier than `g`.
225
+ fn format_general(value: f64, precision: usize, default_type: bool, uppercase: bool) -> String {
226
+ let precision = precision.max(1);
227
+ let decimals = precision.saturating_sub(1);
228
+ let scientific = format!("{value:.decimals$e}");
229
+ let (mantissa, exponent) = scientific.split_once('e').expect("Rust scientific format");
230
+ let exponent = exponent.parse::<i32>().expect("Rust scientific exponent");
231
+ let threshold = if default_type {
232
+ precision.saturating_sub(1)
233
+ } else {
234
+ precision
235
+ };
236
+ let use_scientific = exponent < -4 || usize::try_from(exponent).is_ok_and(|e| e >= threshold);
237
+ if use_scientific {
238
+ let mantissa = trim_fraction(mantissa, false);
239
+ return format!(
240
+ "{mantissa}{}{exponent:+03}",
241
+ if uppercase { 'E' } else { 'e' }
242
+ );
243
+ }
244
+ let decimals = usize::try_from(precision as i128 - i128::from(exponent) - 1)
245
+ .expect("fixed precision is nonnegative");
246
+ trim_fraction(&format!("{value:.decimals$}"), default_type)
247
+ }
248
+
249
+ fn trim_fraction(value: &str, preserve_decimal: bool) -> String {
250
+ let Some((integer, fraction)) = value.split_once('.') else {
251
+ return value.to_string();
252
+ };
253
+ let fraction = fraction.trim_end_matches('0');
254
+ if fraction.is_empty() {
255
+ if preserve_decimal {
256
+ format!("{integer}.0")
257
+ } else {
258
+ integer.to_string()
259
+ }
260
+ } else {
261
+ format!("{integer}.{fraction}")
262
+ }
263
+ }
264
+
265
+ /// Insert thousands separators in the leading digits of an unsigned `body`, first zero-filling
266
+ /// those digits until the grouped field reaches `minimum` characters. As in CPython, a separator
267
+ /// can make the result one character wider than `minimum`.
268
+ fn group_decimal(body: &str, minimum: usize) -> String {
269
+ let digits = body.bytes().take_while(u8::is_ascii_digit).count();
270
+ if digits == 0 {
271
+ return body.into();
272
+ }
273
+ let rest = &body[digits..];
274
+ let mut filled = digits;
275
+ while filled + (filled - 1) / 3 + rest.len() < minimum {
276
+ filled += 1;
277
+ }
278
+ let padded = format!("{}{}", "0".repeat(filled - digits), &body[..digits]);
279
+ let mut result = String::with_capacity(filled + filled / 3 + rest.len());
280
+ for (index, digit) in padded.chars().enumerate() {
281
+ if index > 0 && (filled - index).is_multiple_of(3) {
282
+ result.push(',');
283
+ }
284
+ result.push(digit);
285
+ }
286
+ result.push_str(rest);
287
+ result
288
+ }
289
+
290
+ fn align(value: &str, width: usize, alignment: Align) -> String {
291
+ let padding = width.saturating_sub(value.chars().count());
292
+ let left = match alignment {
293
+ Align::Right => padding,
294
+ Align::Center => padding / 2,
295
+ Align::Left => 0,
296
+ };
297
+ let right = padding - left;
298
+ format!("{}{}{}", " ".repeat(left), value, " ".repeat(right))
299
+ }
300
+
301
+ #[cfg(test)]
302
+ mod tests {
303
+ use super::*;
304
+
305
+ fn float(value: f64, spec: &str) -> String {
306
+ format_float(value, &value.to_string(), &FormatSpec::parse(spec).unwrap()).unwrap()
307
+ }
308
+
309
+ fn integer(value: i64, spec: &str) -> Result<String, String> {
310
+ format_integer(value.into(), &FormatSpec::parse(spec)?)
311
+ }
312
+
313
+ #[test]
314
+ fn parses_every_supported_field() {
315
+ assert_eq!(
316
+ FormatSpec::parse(">+#012,.3f").unwrap(),
317
+ FormatSpec {
318
+ align: Some(Align::Right),
319
+ plus: true,
320
+ alternate: true,
321
+ zero: true,
322
+ width: 12,
323
+ grouping: true,
324
+ precision: Some(3),
325
+ presentation: Some('f'),
326
+ }
327
+ );
328
+ for invalid in ["*<5", "1,,.2", "5.2ff", ".f", " d"] {
329
+ assert!(FormatSpec::parse(invalid).is_err(), "{invalid}");
330
+ }
331
+ }
332
+
333
+ #[test]
334
+ fn grouping_zero_fills_before_inserting_separators() {
335
+ assert_eq!(float(-1234.5, "+12,.2f"), " -1,234.50");
336
+ assert_eq!(float(1234.0, "012,.0f"), "0,000,001,234");
337
+ assert_eq!(float(12345.0, ",.0"), "1e+04");
338
+ assert_eq!(integer(1234, "+012,d").unwrap(), "+000,001,234");
339
+ assert_eq!(integer(-1234567, ",").unwrap(), "-1,234,567");
340
+ assert!(integer(42, ",x").is_err());
341
+ }
342
+
343
+ #[test]
344
+ fn percent_and_signs_match_python() {
345
+ assert_eq!(float(0.125, ".1%"), "12.5%");
346
+ assert_eq!(float(-0.5, "+08.1%"), "-0050.0%");
347
+ assert_eq!(float(f64::INFINITY, "+%"), "+inf%");
348
+ assert_eq!(float(f64::NAN, "E"), "NAN");
349
+ assert_eq!(integer(255, "#06x").unwrap(), "0x00ff");
350
+ assert_eq!(integer(5, "^+5").unwrap(), " +5 ");
351
+ }
352
+ }