vis-agent 0.2.35.dev7414__tar.gz → 0.2.37.dev7443__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 (99) hide show
  1. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/PKG-INFO +1 -1
  2. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/toggle.json +1 -1
  3. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/pyproject.toml +1 -1
  4. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/_contracts.py +51 -5
  5. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/_outside.py +8 -1
  6. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/extension.py +19 -6
  7. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_contracts.py +6 -0
  8. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_scoped_settings.py +26 -0
  9. vis_agent-0.2.37.dev7443/tests/test_text_list_arguments.py +89 -0
  10. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/.gitignore +0 -0
  11. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/LICENSE +0 -0
  12. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/README.md +0 -0
  13. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-arguments.json +0 -0
  14. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-cases.json +0 -0
  15. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-copy.json +0 -0
  16. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-execution.json +0 -0
  17. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-groups.json +0 -0
  18. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-read-session.json +0 -0
  19. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-reads.json +0 -0
  20. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-results.json +0 -0
  21. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity-tables.json +0 -0
  22. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/activity.json +0 -0
  23. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/diff.json +0 -0
  24. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/live-primitives.json +0 -0
  25. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/session-health.json +0 -0
  26. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/fixtures/view.json +0 -0
  27. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/activity.json +0 -0
  28. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/agents.json +0 -0
  29. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/automations.json +0 -0
  30. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/common.json +0 -0
  31. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/config.json +0 -0
  32. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/content.json +0 -0
  33. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/council.json +0 -0
  34. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/diff.json +0 -0
  35. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/gateway.json +0 -0
  36. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/improve.json +0 -0
  37. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/provider.json +0 -0
  38. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/rooms.json +0 -0
  39. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/symbol.json +0 -0
  40. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/contracts/schema/view.json +0 -0
  41. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/examples/greeter/tests/test_greeter.py +0 -0
  42. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/hatch_build.py +0 -0
  43. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/__init__.py +0 -0
  44. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/_wire.py +0 -0
  45. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/activity.py +0 -0
  46. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/__init__.py +0 -0
  47. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_agent.py +0 -0
  48. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_agents.py +0 -0
  49. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_client.py +0 -0
  50. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_council.py +0 -0
  51. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_extensions.py +0 -0
  52. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/engine/_local.py +0 -0
  53. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/extension_package.py +0 -0
  54. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/py.typed +0 -0
  55. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/rooms.py +0 -0
  56. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/src/blockether/vis/views.py +0 -0
  57. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/conftest.py +0 -0
  58. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_activity.py +0 -0
  59. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_agent.py +0 -0
  60. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_agent_extension_lifecycle.py +0 -0
  61. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_agent_extensions.py +0 -0
  62. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_agents.py +0 -0
  63. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_api_guides.py +0 -0
  64. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_authoring.py +0 -0
  65. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_catalog.py +0 -0
  66. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_catalog_recipe.py +0 -0
  67. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_ci_report_recipe.py +0 -0
  68. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_client.py +0 -0
  69. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_client_extensions_engine.py +0 -0
  70. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_code_quality_recipe.py +0 -0
  71. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_council.py +0 -0
  72. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_declarations.py +0 -0
  73. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_engine.py +0 -0
  74. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_engine_docstrings.py +0 -0
  75. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_extension_package.py +0 -0
  76. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_extension_reference.py +0 -0
  77. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_gateway_performance.py +0 -0
  78. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_goals.py +0 -0
  79. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_live_primitives.py +0 -0
  80. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_live_wait.py +0 -0
  81. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_local.py +0 -0
  82. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_method_typing.py +0 -0
  83. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_monitor_recipe.py +0 -0
  84. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_namespace.py +0 -0
  85. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_outside.py +0 -0
  86. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_providers.py +0 -0
  87. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_record_arguments.py +0 -0
  88. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_registration.py +0 -0
  89. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_repair_hooks.py +0 -0
  90. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_result_sequence.py +0 -0
  91. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_rooms.py +0 -0
  92. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_sdk_bootstrap.py +0 -0
  93. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_sdk_guide.py +0 -0
  94. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_sdk_typing.py +0 -0
  95. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_structured_live.py +0 -0
  96. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_symbol_contract.py +0 -0
  97. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_testing.py +0 -0
  98. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_tools.py +0 -0
  99. {vis_agent-0.2.35.dev7414 → vis_agent-0.2.37.dev7443}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vis-agent
3
- Version: 0.2.35.dev7414
3
+ Version: 0.2.37.dev7443
4
4
  Summary: The Vis Python SDK — author extensions and control local or remote engines.
5
5
  Project-URL: Homepage, https://vis.blockether.com
6
6
  Project-URL: Documentation, https://vis.blockether.com
@@ -46,7 +46,7 @@
46
46
  "description": {
47
47
  "type": "string",
48
48
  "minLength": 1,
49
- "maxLength": 100,
49
+ "maxLength": 300,
50
50
  "pattern": "^[^\\r\\n]+$"
51
51
  },
52
52
  "default": {},
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.2.35.dev7414"
11
+ version = "0.2.37.dev7443"
12
12
  description = "The Vis Python SDK — author extensions and control local or remote engines."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.11"
@@ -74,16 +74,62 @@ def _json_value(item):
74
74
  )
75
75
 
76
76
 
77
+ def _problem(error) -> str:
78
+ """One readable line for a schema error: field, rule and limit, never the value."""
79
+ field = ".".join(str(part) for part in error.absolute_path) or "declaration"
80
+ rule, limit = error.validator, error.validator_value
81
+ if rule in ("maxLength", "minLength"):
82
+ bound = "maximum" if rule == "maxLength" else "minimum"
83
+ fix = f" Shorten the {field}." if rule == "maxLength" else ""
84
+ return (
85
+ f"{field} is {len(error.instance)} characters; {bound} is {limit} ({rule})."
86
+ + fix
87
+ )
88
+ if rule == "required":
89
+ missing = ", ".join(key for key in limit if key not in error.instance)
90
+ return f"{missing} is missing (required)."
91
+ if rule == "enum":
92
+ return f"{field} must be one of: {', '.join(map(str, limit))} (enum)."
93
+ if rule == "pattern":
94
+ if (
95
+ limit
96
+ == definition("toggle", "contribution")["properties"]["description"][
97
+ "pattern"
98
+ ]
99
+ ):
100
+ return f"{field} must be one line without line breaks (pattern)."
101
+ return f"{field} does not match the pattern {limit} (pattern)."
102
+ return f"{field} breaks the {rule} rule."
103
+
104
+
105
+ def problems(name: str, definition: str, value: Any) -> list[str]:
106
+ """Readable rule failures of `value`, one line for each. Empty when it is valid.
107
+
108
+ Each line names the field, the rule and its limit. It never echoes the value.
109
+ """
110
+ if not _json_value(value):
111
+ return ["the value is not JSON data."]
112
+ errors = _validator(name, definition).iter_errors(value)
113
+ return [
114
+ _problem(error)
115
+ for error in sorted(errors, key=lambda e: list(map(str, e.absolute_path)))
116
+ ]
117
+
118
+
77
119
  def validate(name: str, definition: str, value: Any) -> Any:
78
120
  """Validate portable JSON data, returning it or raising a payload-free ValueError.
79
121
 
80
- Schemas resolve only from the installed package. Runtime lifecycle, IO and
122
+ The error names each failed field, rule and limit, never the value. Schemas
123
+ resolve only from the installed package. Runtime lifecycle, IO and
81
124
  authorization belong to the engine, not JSON Schema.
82
125
  """
83
126
  try:
84
- valid = _json_value(value) and _validator(name, definition).is_valid(value)
127
+ found = problems(name, definition, value)
85
128
  except (RecursionError, TypeError, ValueError):
86
- valid = False
87
- if not valid:
88
- raise ValueError(f"invalid {name}.{definition} contract")
129
+ found = [""]
130
+ if found:
131
+ detail = " ".join(problem for problem in found if problem)
132
+ raise ValueError(
133
+ f"invalid {name}.{definition} contract" + (f": {detail}" if detail else "")
134
+ )
89
135
  return value
@@ -91,13 +91,20 @@ def workspace_root():
91
91
 
92
92
  def setting_declaration(spec):
93
93
  """Use the package's own JSON Schemas for outside-host declarations."""
94
- from ._contracts import definition, validate
94
+ from ._contracts import definition, problems, validate
95
95
 
96
96
  spec = dict(spec)
97
97
  spec.setdefault(
98
98
  "scopes",
99
99
  definition("toggle", "contribution")["properties"]["scopes"]["default"],
100
100
  )
101
+ found = problems("toggle", "contribution", spec)
102
+ if found:
103
+ name = spec.get("id")
104
+ label = (
105
+ f"Invalid setting {name}" if isinstance(name, str) else "Invalid setting"
106
+ )
107
+ raise ValueError(f"{label}: {' '.join(found)}")
101
108
  return validate("toggle", "contribution", spec)
102
109
 
103
110
 
@@ -1388,10 +1388,11 @@ def _call_arguments(fn, args, kwargs):
1388
1388
  An argument crosses the host boundary as JSON, so a record arrives as a dict of its
1389
1389
  public fields. A parameter annotated with a dataclass gets that dataclass back,
1390
1390
  built through its constructor. This also works inside a union, list, tuple, set or
1391
- dict annotation (Blockether/vis#289). Annotations resolve statically, as for tool
1392
- contracts. A value that does not fit its annotation stays as it arrived. So does
1393
- every argument of a call that the signature cannot bind: the call itself reports
1394
- that mistake.
1391
+ dict annotation (Blockether/vis#289). One bare string for a sequence of strings
1392
+ becomes one item, not one item for each character (Blockether/vis#324).
1393
+ Annotations resolve statically, as for tool contracts. A value that does not fit
1394
+ its annotation stays as it arrived. So does every argument of a call that the
1395
+ signature cannot bind: the call itself reports that mistake.
1395
1396
  """
1396
1397
  try:
1397
1398
  # A wrapper around a bound method must not unwrap past the binding.
@@ -1561,9 +1562,10 @@ def _holds_record(shape):
1561
1562
  def _rebuilt_value(shape, value):
1562
1563
  """Return `value` with the records that `shape` expects rebuilt.
1563
1564
 
1565
+ One bare string for a sequence of strings becomes that sequence with one item.
1564
1566
  A value that does not fit stays as is.
1565
1567
  """
1566
- if not _holds_record(shape):
1568
+ if not (_holds_record(shape) or isinstance(value, str)):
1567
1569
  return value
1568
1570
  result = _fitted_argument(shape, value)
1569
1571
  return value if result is _UNFIT else result
@@ -1588,7 +1590,11 @@ def _fitted_argument(shape, value):
1588
1590
  if kind == "record":
1589
1591
  return _rebuilt_record(shape[1], value, shape[2])
1590
1592
  if kind == "union":
1591
- for member in shape[1]:
1593
+ members = shape[1]
1594
+ if isinstance(value, str):
1595
+ # A member that takes the string as it is wins over one that wraps it.
1596
+ members = sorted(members, key=lambda member: member[0] == "sequence")
1597
+ for member in members:
1592
1598
  result = _fitted_argument(member, value)
1593
1599
  if result is not _UNFIT:
1594
1600
  return result
@@ -1599,6 +1605,13 @@ def _fitted_argument(shape, value):
1599
1605
  if not _holds_record(shape):
1600
1606
  return value
1601
1607
  return {key: _rebuilt_value(shape[1], item) for key, item in value.items()}
1608
+ if kind == "sequence" and isinstance(value, str):
1609
+ # A string is a sequence of its characters; the caller named one item. An
1610
+ # item of any type also takes the whole string, so it stays as it is.
1611
+ if shape[2] in (("any",), ("plain", object)):
1612
+ return _UNFIT
1613
+ item = _fitted_argument(shape[2], value)
1614
+ return _UNFIT if item is _UNFIT else shape[1]((item,))
1602
1615
  if not isinstance(value, (list, tuple)):
1603
1616
  return _UNFIT
1604
1617
  if kind == "fixed":
@@ -178,6 +178,12 @@ def test_payload_free_validation():
178
178
  with pytest.raises(ValueError) as error:
179
179
  _contracts.validate("activity", "declaration", bad)
180
180
  assert "do-not-echo" not in str(error.value)
181
+ with pytest.raises(
182
+ ValueError, match=r": label does not match the pattern .* \(pattern\)"
183
+ ):
184
+ _contracts.validate(
185
+ "activity", "declaration", {"presenter": "tests", "label": "one\ntwo"}
186
+ )
181
187
  with pytest.raises(ValueError):
182
188
  _contracts.validate("view", "missing-definition", {})
183
189
 
@@ -68,6 +68,32 @@ def test_parent_nests_a_setting():
68
68
  vis.Setting(id="test_detail", label="Test detail", default=False, parent=" ")
69
69
 
70
70
 
71
+ # Regression for #322 and #323: a 300-character description is valid, and a
72
+ # longer one names the setting, the field, the rule and the limit.
73
+ def test_description_limit_and_actionable_error():
74
+ def mode(description):
75
+ return vis.Setting(
76
+ id="steering_mode",
77
+ label="Steering mode",
78
+ type="enum",
79
+ choices=["vibe", "control"],
80
+ default="vibe",
81
+ description=description,
82
+ )
83
+
84
+ assert mode("x" * 300).description == "x" * 300
85
+ with pytest.raises(ValueError) as error:
86
+ mode("x" * 301)
87
+ assert str(error.value) == (
88
+ "Invalid setting steering_mode: description is 301 characters; "
89
+ "maximum is 300 (maxLength). Shorten the description."
90
+ )
91
+ with pytest.raises(ValueError, match="one line without line breaks"):
92
+ mode("two\nlines")
93
+ with pytest.raises(ValueError, match="minimum is 1"):
94
+ mode("")
95
+
96
+
71
97
  def test_application_bridge_rejects_host_only_settings_before_connecting():
72
98
  declaration = vis.Extension(
73
99
  name="Settings test",
@@ -0,0 +1,89 @@
1
+ """Regression coverage for #324: one bare string for a list of strings is one item."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ from typing import Annotated, Literal, Optional
8
+
9
+ import blockether.vis.extension as vis
10
+
11
+
12
+ class Tools:
13
+ """The parameter shapes of the `py.*` and `clj.*` language tools."""
14
+
15
+ def lint_code(
16
+ self,
17
+ paths: Annotated[Sequence[str], "Files or directories to lint."] = (),
18
+ *,
19
+ cwd: str = "",
20
+ namespaces: Annotated[Sequence[str], "Test namespaces to run."] = (),
21
+ ) -> tuple:
22
+ return tuple(paths), tuple(namespaces)
23
+
24
+
25
+ def rebuilt(fn, *args, **kwargs):
26
+ return vis._call_arguments(fn, list(args), kwargs)
27
+
28
+
29
+ def test_bare_string_for_a_string_sequence_is_one_item():
30
+ # Regression for Blockether/vis#324: `py.lint_code("/src/app.py")` named one
31
+ # path for each character, and `/` made the lint walk the whole filesystem.
32
+ tools = Tools()
33
+ args, kwargs = rebuilt(tools.lint_code, "/src/app.py", namespaces="my.ns-test")
34
+ assert args == [["/src/app.py"]]
35
+ assert kwargs == {"namespaces": ["my.ns-test"]}
36
+ assert tools.lint_code(*args, **kwargs) == (("/src/app.py",), ("my.ns-test",))
37
+ assert rebuilt(tools.lint_code, paths="a.py")[1] == {"paths": ["a.py"]}
38
+
39
+
40
+ def test_lists_and_other_parameters_stay_as_they_arrived():
41
+ tools = Tools()
42
+ paths = ["a.py", "b.py"]
43
+ args, kwargs = rebuilt(tools.lint_code, paths, cwd="/src")
44
+ assert args[0] is paths
45
+ assert kwargs == {"cwd": "/src"}
46
+
47
+
48
+ def test_container_and_item_annotations_decide_the_shape():
49
+ def files(
50
+ paths: list[str], tags: set[str] = frozenset(), pair: tuple[str, ...] = ()
51
+ ):
52
+ return paths, tags, pair
53
+
54
+ def optional(paths: Optional[list[str]] = None, more: list[str] | None = None): # noqa: UP045
55
+ return paths, more
56
+
57
+ def literal(modes: list[Literal["fast", "full"]] = ()):
58
+ return modes
59
+
60
+ assert rebuilt(files, "a.py", tags="x", pair="y") == (
61
+ [["a.py"]],
62
+ {"tags": {"x"}, "pair": ("y",)},
63
+ )
64
+ assert rebuilt(optional, "a.py", more="b.py") == ([["a.py"]], {"more": ["b.py"]})
65
+ assert rebuilt(literal, "fast") == ([["fast"]], {})
66
+ assert rebuilt(literal, "other") == (["other"], {})
67
+
68
+
69
+ def test_a_member_that_takes_the_string_as_it_is_wins():
70
+ def either(paths: list[str] | str = (), path: str | list[str] = ""):
71
+ return paths, path
72
+
73
+ def anything(
74
+ values: list = (), items: Sequence[object] = (), raw: list[bytes] = ()
75
+ ):
76
+ return values, items, raw
77
+
78
+ assert rebuilt(either, "a.py", path="b.py") == (["a.py"], {"path": "b.py"})
79
+ assert rebuilt(anything, "ab", items="cd", raw="ef") == (
80
+ ["ab"],
81
+ {"items": "cd", "raw": "ef"},
82
+ )
83
+
84
+
85
+ def test_path_items_take_a_bare_string_unchanged():
86
+ def walk(paths: list[str | Path] = ()):
87
+ return paths
88
+
89
+ assert rebuilt(walk, "src") == ([["src"]], {})