virtualshell 1.2.0__tar.gz → 1.2.1__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 (79) hide show
  1. {virtualshell-1.2.0 → virtualshell-1.2.1}/PKG-INFO +10 -1
  2. {virtualshell-1.2.0 → virtualshell-1.2.1}/README.md +9 -0
  3. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/binder.cpp +1 -1
  4. {virtualshell-1.2.0 → virtualshell-1.2.1}/pyproject.toml +1 -1
  5. virtualshell-1.2.1/src/virtualshell/_version.py +1 -0
  6. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/generate_psobject.py +62 -15
  7. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/ps_proxy.py +173 -8
  8. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/shell.py +16 -5
  9. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_generate_psobject.py +67 -0
  10. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_ps_proxy_integration.py +90 -0
  11. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_ps_proxy_unit.py +53 -0
  12. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_shell_unit.py +34 -7
  13. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/API Overview.md +1 -1
  14. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/generate_psobject.md +20 -0
  15. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/make_proxy.md +43 -3
  16. virtualshell-1.2.0/src/virtualshell/_version.py +0 -1
  17. {virtualshell-1.2.0 → virtualshell-1.2.1}/.github/workflows/test_build-only.yml +0 -0
  18. {virtualshell-1.2.0 → virtualshell-1.2.1}/.github/workflows/workflow.yml +0 -0
  19. {virtualshell-1.2.0 → virtualshell-1.2.1}/.gitignore +0 -0
  20. {virtualshell-1.2.0 → virtualshell-1.2.1}/CMakeLists.txt +0 -0
  21. {virtualshell-1.2.0 → virtualshell-1.2.1}/CMakeSettings.json +0 -0
  22. {virtualshell-1.2.0 → virtualshell-1.2.1}/LICENSE +0 -0
  23. {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/bench.csv +0 -0
  24. {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/bench.json +0 -0
  25. {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/vs_bench.py +0 -0
  26. {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/zcb_bench.json +0 -0
  27. {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/zcb_bench.py +0 -0
  28. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/cmd_state.hpp +0 -0
  29. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/config.hpp +0 -0
  30. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/dev_debug.hpp +0 -0
  31. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/execution_result.hpp +0 -0
  32. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/helpers.hpp +0 -0
  33. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/io_pump.hpp +0 -0
  34. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/powershell_process.hpp +0 -0
  35. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/process.hpp +0 -0
  36. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/py_bridge.hpp +0 -0
  37. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/timeout_watcher.hpp +0 -0
  38. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/virtual_shell.hpp +0 -0
  39. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/io_pump.cpp +0 -0
  40. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/powershell_process.cpp +0 -0
  41. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/virtual_shell.cpp +0 -0
  42. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/CMakeLists.txt +0 -0
  43. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_framework.hpp +0 -0
  44. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_helpers.cpp +0 -0
  45. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_main.cpp +0 -0
  46. {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_virtual_shell.cpp +0 -0
  47. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/__init__.py +0 -0
  48. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_bootstrap.py +0 -0
  49. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_module.py +0 -0
  50. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_protocols.py +0 -0
  51. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/errors.py +0 -0
  52. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/get-session.ps1 +0 -0
  53. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/ps_object.py +0 -0
  54. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/save-session.ps1 +0 -0
  55. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/zero_copy_bridge.ps1 +0 -0
  56. {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/zero_copy_bridge_shell.py +0 -0
  57. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/conftest.py +0 -0
  58. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_integration.py +0 -0
  59. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_psobject_unit.py +0 -0
  60. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_utils_unit.py +0 -0
  61. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_zcb_channel_unit.py +0 -0
  62. {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_zcb_integration.py +0 -0
  63. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Getting started.md +0 -0
  64. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Installation.md +0 -0
  65. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Quickstart.md +0 -0
  66. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Help/FAQ.md +0 -0
  67. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Help/Troubleshooting.md +0 -0
  68. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Home.md +0 -0
  69. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Benchmarks.md +0 -0
  70. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Changelog.md +0 -0
  71. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Design & Architecture.md +0 -0
  72. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Asynchronous Execution.md +0 -0
  73. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Configuration.md +0 -0
  74. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Error Handling.md +0 -0
  75. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Performance Tips.md +0 -0
  76. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Running Scripts.md +0 -0
  77. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Security Notes.md +0 -0
  78. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Synchronous Execution.md +0 -0
  79. {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Zero-Copy Bridge.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: virtualshell
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: High-performance PowerShell bridge (C++ pybind11 backend)
5
5
  Keywords: powershell,automation,shell,cpp,pybind11
6
6
  Author: Kim-Andre Myrvold
@@ -393,6 +393,15 @@ with Shell() as sh:
393
393
  writer.Close()
394
394
  ```
395
395
 
396
+ A bare `[Type]` literal binds the type itself, exposing its **static**
397
+ methods, properties and constants:
398
+
399
+ ```python
400
+ with Shell() as sh:
401
+ math = sh.make_proxy("", "[System.Math]")
402
+ print(math.Sqrt(16.0), math.PI) # 4.0 3.141592653589793
403
+ ```
404
+
396
405
  Pair proxies with `generate_psobject`, which produces typed `Protocol` stubs
397
406
  (including overloads) for IDE completion and static analysis:
398
407
 
@@ -169,6 +169,15 @@ with Shell() as sh:
169
169
  writer.Close()
170
170
  ```
171
171
 
172
+ A bare `[Type]` literal binds the type itself, exposing its **static**
173
+ methods, properties and constants:
174
+
175
+ ```python
176
+ with Shell() as sh:
177
+ math = sh.make_proxy("", "[System.Math]")
178
+ print(math.Sqrt(16.0), math.PI) # 4.0 3.141592653589793
179
+ ```
180
+
172
181
  Pair proxies with `generate_psobject`, which produces typed `Protocol` stubs
173
182
  (including overloads) for IDE completion and static analysis:
174
183
 
@@ -264,7 +264,7 @@ PYBIND11_MODULE(_core, m) {
264
264
  }, "Create a new VirtualShell instance", py::arg("config"));
265
265
 
266
266
  // Metadata
267
- m.attr("__version__") = "1.2.0";
267
+ m.attr("__version__") = "1.2.1";
268
268
  m.attr("__author__") = "Kim-Andre Myrvold";
269
269
  }
270
270
 
@@ -9,7 +9,7 @@ build-backend = "scikit_build_core.build"
9
9
  [project]
10
10
  name = "virtualshell"
11
11
  description = "High-performance PowerShell bridge (C++ pybind11 backend)"
12
- version = "1.2.0"
12
+ version = "1.2.1"
13
13
  readme = "README.md"
14
14
  license = { file = "LICENSE" }
15
15
  authors = [{ name = "Kim-Andre Myrvold" }]
@@ -0,0 +1 @@
1
+ version = "1.2.1"
@@ -413,14 +413,17 @@ def _property_type(entry: MutableMapping[str, Any]) -> str:
413
413
  if isinstance(definition, list) and definition:
414
414
  definition = definition[0]
415
415
  if isinstance(definition, str):
416
- token = definition.strip().split(" ", 1)[0]
416
+ # Static definitions read "static datetime Now {get;}".
417
+ text = re.sub(r"^\s*static\s+", "", definition.strip())
418
+ token = text.split(" ", 1)[0]
417
419
  if token and TYPE_LIKE_PATTERN.fullmatch(token):
418
420
  return token
419
421
  return ""
420
422
 
421
423
 
422
424
  def render_protocol(class_name: str, members: Iterable[MutableMapping[str, Any]], *,
423
- ps_type_name: str = "", ps_expression: str = "") -> str:
425
+ ps_type_name: str = "", ps_expression: str = "",
426
+ ps_static: bool = False) -> str:
424
427
  grouped = categorize_members(members)
425
428
  typing_bits: Set[str] = {"Protocol"}
426
429
  runtime_bits: Set[str] = set()
@@ -472,6 +475,8 @@ def render_protocol(class_name: str, members: Iterable[MutableMapping[str, Any]]
472
475
  meta_lines.append(f" __ps_type_name__: ClassVar[str] = {ps_type_name!r}")
473
476
  if ps_expression:
474
477
  meta_lines.append(f" __ps_expression__: ClassVar[str] = {ps_expression!r}")
478
+ if ps_static:
479
+ meta_lines.append(" __ps_static__: ClassVar[bool] = True")
475
480
  if meta_lines:
476
481
  typing_bits.add("ClassVar")
477
482
 
@@ -509,6 +514,21 @@ def render_protocol(class_name: str, members: Iterable[MutableMapping[str, Any]]
509
514
  return "\n".join(lines)
510
515
 
511
516
 
517
+ def _decode_members(raw_text: str) -> List[MutableMapping[str, Any]]:
518
+ if not raw_text:
519
+ raise RuntimeError("Get-Member returned no data")
520
+ try:
521
+ members: Any = json.loads(raw_text)
522
+ except json.JSONDecodeError as exc:
523
+ raise RuntimeError("Failed to parse Get-Member output as JSON") from exc
524
+
525
+ if isinstance(members, MutableMapping):
526
+ members = [members]
527
+ if not members:
528
+ raise RuntimeError("Get-Member produced an empty result set")
529
+ return members
530
+
531
+
512
532
  def fetch_members(shell, command: str) -> Tuple[str, List[MutableMapping[str, Any]]]:
513
533
  shell.run("Remove-Variable obj -ErrorAction SilentlyContinue", raise_on_error=False)
514
534
  assignment = f"$obj = ({command})"
@@ -525,20 +545,28 @@ def fetch_members(shell, command: str) -> Tuple[str, List[MutableMapping[str, An
525
545
  "Get-Member -InputObject $obj | ConvertTo-Json -Depth 6 -Compress",
526
546
  raise_on_error=True,
527
547
  )
528
- raw_text = (raw_result.out or "").strip()
529
- if not raw_text:
530
- raise RuntimeError("Get-Member returned no data")
548
+ return type_name, _decode_members((raw_result.out or "").strip())
531
549
 
532
- try:
533
- members: Any = json.loads(raw_text)
534
- except json.JSONDecodeError as exc:
535
- raise RuntimeError("Failed to parse Get-Member output as JSON") from exc
536
550
 
537
- if isinstance(members, MutableMapping):
538
- members = [members]
539
- if not members:
540
- raise RuntimeError("Get-Member produced an empty result set")
541
- return type_name, members
551
+ def fetch_static_members(shell, type_text: str) -> Tuple[str, List[MutableMapping[str, Any]]]:
552
+ """Static members of ``[type_text]``.
553
+
554
+ Piping (not -InputObject) matters here: Get-Member special-cases a piped
555
+ System.Type with -Static and reports that type's own static members.
556
+ """
557
+ shell.run("Remove-Variable obj -ErrorAction SilentlyContinue", raise_on_error=False)
558
+ shell.run(f"$obj = [{type_text}]", raise_on_error=True)
559
+
560
+ type_result = shell.run("[string]$obj.FullName", raise_on_error=True)
561
+ type_name = (type_result.out or "").strip()
562
+ if not type_name:
563
+ raise RuntimeError("Type reports no name")
564
+
565
+ raw_result = shell.run(
566
+ "$obj | Get-Member -Static | ConvertTo-Json -Depth 6 -Compress",
567
+ raise_on_error=True,
568
+ )
569
+ return type_name, _decode_members((raw_result.out or "").strip())
542
570
 
543
571
 
544
572
  def safe_class_name(type_name: str) -> str:
@@ -556,13 +584,32 @@ def generate(shell, obj: str, output_path: Path) -> None:
556
584
  shell.run("$PSStyle.OutputRendering = 'PlainText'", raise_on_error=False)
557
585
  shell.run("$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new()", raise_on_error=False)
558
586
 
559
- from .ps_proxy import build_creation_strategies
587
+ from .ps_proxy import build_creation_strategies, static_type_literal
560
588
 
561
589
  strategies = build_creation_strategies(obj)
562
590
  errors: List[str] = []
563
591
  chosen: Optional[Tuple[str, str, str, List[MutableMapping[str, Any]]]] = None
564
592
 
565
593
  try:
594
+ static_inner = static_type_literal(obj)
595
+ if static_inner is not None:
596
+ try:
597
+ type_name, members = fetch_static_members(shell, static_inner)
598
+ except Exception as ex:
599
+ raise RuntimeError(
600
+ f"Unable to materialise a static type from '{obj}': {ex}") from ex
601
+ expression = f"[{static_inner}]"
602
+ # Name the class after the caller's spelling: the reflected
603
+ # FullName of a closed generic is assembly-qualified soup.
604
+ protocol_name = safe_class_name(static_inner)
605
+ source = render_protocol(protocol_name, members,
606
+ ps_type_name=type_name,
607
+ ps_expression=expression, ps_static=True)
608
+ output_path.write_text(source, encoding="utf-8")
609
+ print(f"Generated {output_path} for static {type_name} "
610
+ f"(expression: {expression})")
611
+ return
612
+
566
613
  for label, candidate in strategies:
567
614
  try:
568
615
  type_name, members = fetch_members(shell, candidate)
@@ -66,6 +66,53 @@ def _method_call_expr(ref: str, name: str, args: List[str]) -> str:
66
66
  return f"{ref}.PSObject.Methods[{_ps_quote(name)}].Invoke({arg_text})"
67
67
 
68
68
 
69
+ # `ref` holds a [type] object in static mode; PowerShell resolves `::` against
70
+ # the contained type, so `$t = [Math]; $t::Sqrt(16)` works like `[Math]::Sqrt`.
71
+ _STATIC_BINDING_FLAGS = "[System.Reflection.BindingFlags]'Public,Static"
72
+
73
+
74
+ def _static_member_expr(ref: str, name: str) -> str:
75
+ """Expression reading static member `name` on the type held by `ref`."""
76
+ if _IDENT_RE.match(name):
77
+ return f"{ref}::{name}"
78
+ return (f"{ref}.InvokeMember({_ps_quote(name)}, "
79
+ f"{_STATIC_BINDING_FLAGS},GetProperty,GetField', $null, $null, @())")
80
+
81
+
82
+ def _static_method_call_expr(ref: str, name: str, args: List[str]) -> str:
83
+ arg_text = ", ".join(args)
84
+ if _IDENT_RE.match(name):
85
+ return f"{ref}::{name}({arg_text})"
86
+ return (f"{ref}.InvokeMember({_ps_quote(name)}, "
87
+ f"{_STATIC_BINDING_FLAGS},InvokeMethod', $null, $null, @({arg_text}))")
88
+
89
+
90
+ def static_type_literal(expr: str) -> Optional[str]:
91
+ """Return the type name inside a bare ``[Type]`` literal, else None.
92
+
93
+ A bare literal (nothing outside the brackets) is how callers name a
94
+ static class, e.g. ``[System.Windows.Forms.MessageBox]``. Invocations
95
+ (``[T]::new()``) and casts (``[int](3)``) do not match, and neither do
96
+ unbalanced brackets, so generic literals like
97
+ ``[System.Collections.Generic.List[int]]`` resolve correctly.
98
+ """
99
+ cleaned = (expr or "").strip()
100
+ if len(cleaned) < 3 or not (cleaned.startswith("[") and cleaned.endswith("]")):
101
+ return None
102
+ inner = cleaned[1:-1].strip()
103
+ if not inner or not _TYPE_LIKE_RE.fullmatch(inner):
104
+ return None
105
+ depth = 0
106
+ for char in inner:
107
+ if char == "[":
108
+ depth += 1
109
+ elif char == "]":
110
+ depth -= 1
111
+ if depth < 0:
112
+ return None
113
+ return inner if depth == 0 else None
114
+
115
+
69
116
  def split_invocation(expr: str) -> Optional[Tuple[str, str]]:
70
117
  """Return (target, argument_text) when `expr` ends with a (...) call."""
71
118
  depth = 0
@@ -243,6 +290,60 @@ $__vs_sm = @($__vs_o.PSObject.Members |
243
290
  """.strip()
244
291
 
245
292
 
293
+ # Static-mode counterpart: {ref} holds a [type] object, so reflection runs on
294
+ # the type itself (not GetType(), which would describe RuntimeType). Constants
295
+ # and static fields surface as properties; writability follows the setter /
296
+ # IsInitOnly / IsLiteral rules.
297
+ _STATIC_SCHEMA_SCRIPT = """
298
+ $__vs_o = {ref}
299
+ $__vs_flags = [System.Reflection.BindingFlags]'Public,Static'
300
+ $__vs_props = @()
301
+ foreach ($__vs_pi in $__vs_o.GetProperties($__vs_flags)) {{
302
+ $__vs_props += [pscustomobject]@{{
303
+ n = $__vs_pi.Name
304
+ t = [string]$__vs_pi.PropertyType.FullName
305
+ w = ($null -ne $__vs_pi.SetMethod)
306
+ }}
307
+ }}
308
+ foreach ($__vs_fi in $__vs_o.GetFields($__vs_flags)) {{
309
+ $__vs_props += [pscustomobject]@{{
310
+ n = $__vs_fi.Name
311
+ t = [string]$__vs_fi.FieldType.FullName
312
+ w = (-not ($__vs_fi.IsInitOnly -or $__vs_fi.IsLiteral))
313
+ }}
314
+ }}
315
+ $__vs_meths = @()
316
+ try {{
317
+ $__vs_groups = $__vs_o.GetMethods($__vs_flags) |
318
+ Where-Object {{ -not $_.IsSpecialName }} |
319
+ Group-Object Name
320
+ }} catch {{ $__vs_groups = @() }}
321
+ foreach ($__vs_g in $__vs_groups) {{
322
+ $__vs_ovl = @(foreach ($__vs_mi in $__vs_g.Group) {{
323
+ [pscustomobject]@{{
324
+ r = $__vs_mi.ReturnType.FullName
325
+ p = @(foreach ($__vs_pp in $__vs_mi.GetParameters()) {{
326
+ [pscustomobject]@{{
327
+ n = $__vs_pp.Name
328
+ t = $__vs_pp.ParameterType.FullName
329
+ o = $__vs_pp.IsOut
330
+ rf = $__vs_pp.ParameterType.IsByRef
331
+ a = $__vs_pp.ParameterType.IsArray
332
+ }}
333
+ }})
334
+ }}
335
+ }})
336
+ $__vs_meths += ,([pscustomobject]@{{ n = $__vs_g.Name; o = $__vs_ovl }})
337
+ }}
338
+ [pscustomobject]@{{
339
+ tn = [string]$__vs_o.FullName
340
+ props = $__vs_props
341
+ meths = $__vs_meths
342
+ sm = @()
343
+ }} | ConvertTo-Json -Depth 8 -Compress
344
+ """.strip()
345
+
346
+
246
347
  def _parse_schema(payload: Dict[str, Any]) -> Schema:
247
348
  schema = Schema(type_name=str(payload.get("tn") or "System.Object"))
248
349
 
@@ -360,6 +461,10 @@ class PsProxy:
360
461
 
361
462
  Attribute access reads/writes properties and calls methods on the
362
463
  underlying object inside the PowerShell session.
464
+
465
+ A bare ``[Type]`` literal (or ``static=True``) selects static mode: the
466
+ proxy binds the type itself and exposes its static methods, properties
467
+ and constants, invoked with ``::`` in the session.
363
468
  """
364
469
 
365
470
  def __init__(
@@ -369,6 +474,7 @@ class PsProxy:
369
474
  object_ref: str = "$obj",
370
475
  *,
371
476
  timeout: Optional[float] = None,
477
+ static: bool = False,
372
478
  ) -> None:
373
479
  object.__setattr__(self, "_shell", shell)
374
480
  object.__setattr__(self, "_timeout", timeout)
@@ -384,10 +490,17 @@ class PsProxy:
384
490
  if not ref:
385
491
  raise ValueError("object_ref or type_name is required")
386
492
 
493
+ literal = static_type_literal(ref)
494
+ object.__setattr__(self, "_static", bool(static) or literal is not None)
495
+
387
496
  if ref.startswith("$"):
388
497
  self._validate_ref(ref)
389
498
  object.__setattr__(self, "_ref", ref)
390
499
  object.__setattr__(self, "_owns_ref", False)
500
+ elif self._static:
501
+ inner = literal if literal is not None else ref
502
+ object.__setattr__(self, "_ref", self._materialize_static(inner))
503
+ object.__setattr__(self, "_owns_ref", True)
391
504
  else:
392
505
  object.__setattr__(self, "_ref", self._materialize(ref))
393
506
  object.__setattr__(self, "_owns_ref", True)
@@ -404,6 +517,18 @@ class PsProxy:
404
517
  return (res.out or "").strip()
405
518
 
406
519
  def _validate_ref(self, ref: str) -> None:
520
+ if self._static:
521
+ out = self._run(
522
+ f"if ({ref} -is [type]) {{ 'ok' }} "
523
+ f"elseif ($null -eq {ref}) {{ 'null' }} else {{ 'notatype' }}",
524
+ label=f"Binding to {ref}")
525
+ if out == "null":
526
+ raise ValueError(f"PowerShell variable {ref} is null or undefined")
527
+ if out != "ok":
528
+ raise ValueError(
529
+ f"PowerShell variable {ref} does not hold a [type]; "
530
+ "a static proxy must bind a type object")
531
+ return
407
532
  out = self._run(
408
533
  f"if ($null -eq {ref}) {{ 'null' }} else {{ 'ok' }}",
409
534
  label=f"Binding to {ref}")
@@ -434,13 +559,34 @@ class PsProxy:
434
559
  f"Failed to create PowerShell object from {expression!r}. Tried:\n "
435
560
  + "\n ".join(errors))
436
561
 
562
+ def _materialize_static(self, type_text: str) -> str:
563
+ """Bind a ``[Type]`` literal to a session variable (static mode)."""
564
+ inner = type_text.strip()
565
+ if inner.startswith("[") and inner.endswith("]"):
566
+ inner = inner[1:-1].strip()
567
+ if not inner or not _TYPE_LIKE_RE.fullmatch(inner):
568
+ raise ValueError(f"Cannot resolve a static type from {type_text!r}")
569
+
570
+ var = _next_var("proxy")
571
+ res = self._shell.run(f"${var} = [{inner}]", timeout=self._timeout)
572
+ if res.success:
573
+ check = self._shell.run(
574
+ f"if (${var} -is [type]) {{ 'ok' }} else {{ 'no' }}",
575
+ timeout=self._timeout)
576
+ if check.success and (check.out or "").strip() == "ok":
577
+ return f"${var}"
578
+ raise RuntimeError(
579
+ f"Failed to resolve static type [{inner}]: "
580
+ f"{(res.err or '').strip()[:200] or 'expression did not yield a type'}")
581
+
437
582
  def _load_schema(self, provided_type: str) -> Schema:
438
583
  run_id = self._shell.python_run_id
439
584
 
440
585
  # A provided type name is only trusted as a cache key after we have
441
586
  # resolved it once; the real type name always comes from the object.
587
+ script = _STATIC_SCHEMA_SCRIPT if self._static else _SCHEMA_SCRIPT
442
588
  out = self._run(
443
- _SCHEMA_SCRIPT.format(ref=self._ref),
589
+ script.format(ref=self._ref),
444
590
  label=f"Schema query for {self._ref}")
445
591
  try:
446
592
  payload = json.loads(out)
@@ -449,8 +595,11 @@ class PsProxy:
449
595
  f"Schema query for {self._ref} returned invalid JSON") from exc
450
596
 
451
597
  schema = _parse_schema(payload)
598
+ # Static schemas get their own key: the same type can also have an
599
+ # instance schema, and _sub_proxy looks instance schemas up by name.
600
+ key = f"static:{schema.type_name}" if self._static else schema.type_name
452
601
  with _SCHEMA_LOCK:
453
- _SCHEMA_CACHE[(run_id, schema.type_name)] = schema
602
+ _SCHEMA_CACHE[(run_id, key)] = schema
454
603
  return schema
455
604
 
456
605
  @classmethod
@@ -466,6 +615,8 @@ class PsProxy:
466
615
  object.__setattr__(proxy, "_method_cache", {})
467
616
  object.__setattr__(proxy, "_ref", ref)
468
617
  object.__setattr__(proxy, "_owns_ref", True)
618
+ # Values returned from calls are instances, never types.
619
+ object.__setattr__(proxy, "_static", False)
469
620
 
470
621
  cached = self._cached_schema(self._shell.python_run_id, type_name)
471
622
  if cached is not None:
@@ -590,9 +741,10 @@ class PsProxy:
590
741
  method_cache[name] = bound
591
742
  return bound
592
743
  if name in schema.properties:
744
+ expr = (_static_member_expr(self._ref, name) if self._static
745
+ else _member_expr(self._ref, name))
593
746
  return self._fetch_value(
594
- _member_expr(self._ref, name),
595
- label=f"Read property {schema.type_name}.{name}")
747
+ expr, label=f"Read property {schema.type_name}.{name}")
596
748
  if name in dynamic:
597
749
  return dynamic[name]
598
750
  raise AttributeError(
@@ -610,11 +762,16 @@ class PsProxy:
610
762
  meta = schema.properties[name]
611
763
  if not meta.writable:
612
764
  raise AttributeError(f"Property {name!r} is read-only")
765
+ if self._static and not _IDENT_RE.match(name):
766
+ raise AttributeError(
767
+ f"Cannot write static member {name!r}: not an identifier")
768
+ target = (_static_member_expr(self._ref, name) if self._static
769
+ else _member_expr(self._ref, name))
613
770
  cleanup: List[str] = []
614
771
  try:
615
772
  rhs = self._format_argument(value, cleanup=cleanup)
616
773
  self._run(
617
- f"{_member_expr(self._ref, name)} = {rhs}",
774
+ f"{target} = {rhs}",
618
775
  label=f"Set property {schema.type_name}.{name}")
619
776
  finally:
620
777
  self._cleanup_vars(cleanup)
@@ -632,7 +789,8 @@ class PsProxy:
632
789
  return sorted(names)
633
790
 
634
791
  def __repr__(self) -> str:
635
- return f"<PsProxy type='{self._schema.type_name}' ref='{self._ref}'>"
792
+ kind = "static type" if self._static else "type"
793
+ return f"<PsProxy {kind}='{self._schema.type_name}' ref='{self._ref}'>"
636
794
 
637
795
  # -- methods ---------------------------------------------------------------
638
796
 
@@ -679,7 +837,10 @@ class PsProxy:
679
837
  ps_args.append(self._format_argument(value,
680
838
  cleanup=cleanup))
681
839
 
682
- call = _method_call_expr(self._ref, meta.name, ps_args)
840
+ if self._static:
841
+ call = _static_method_call_expr(self._ref, meta.name, ps_args)
842
+ else:
843
+ call = _method_call_expr(self._ref, meta.name, ps_args)
683
844
  return_type = overload.return_type if overload else ""
684
845
  if _AWAITABLE_RE.search(return_type):
685
846
  call = f"({call}).GetAwaiter().GetResult()"
@@ -754,7 +915,11 @@ class PsProxy:
754
915
  for one_call in call_args[start:start + batch_size]:
755
916
  ps_args = [self._format_argument(a, cleanup=cleanup)
756
917
  for a in one_call]
757
- call = _method_call_expr(self._ref, method_name, ps_args)
918
+ if self._static:
919
+ call = _static_method_call_expr(
920
+ self._ref, method_name, ps_args)
921
+ else:
922
+ call = _method_call_expr(self._ref, method_name, ps_args)
758
923
  lines.append(f"$__vs_mc.Add(({call}))")
759
924
  lines.append(
760
925
  "ConvertTo-Json -InputObject $__vs_mc -Depth 4 -Compress")
@@ -492,12 +492,15 @@ class Shell:
492
492
  return self._zcb
493
493
 
494
494
  @overload
495
- def make_proxy(self, type_name: str, obj_ref: Optional[str] = None) -> "PsProxy": ...
495
+ def make_proxy(self, type_name: str, obj_ref: Optional[str] = None,
496
+ *, static: bool = False) -> "PsProxy": ...
496
497
 
497
498
  @overload
498
- def make_proxy(self, type_name: Type[_ProxyProtocol], obj_ref: Optional[str] = None) -> _ProxyProtocol: ...
499
+ def make_proxy(self, type_name: Type[_ProxyProtocol], obj_ref: Optional[str] = None,
500
+ *, static: bool = False) -> _ProxyProtocol: ...
499
501
 
500
- def make_proxy(self, type_name: Union[str, Type[Any]], obj_ref: Optional[str] = None) -> Any:
502
+ def make_proxy(self, type_name: Union[str, Type[Any]], obj_ref: Optional[str] = None,
503
+ *, static: bool = False) -> Any:
501
504
  """Create a live proxy for a PowerShell object.
502
505
 
503
506
  Two call forms:
@@ -509,6 +512,11 @@ class Shell:
509
512
  recreates the object, and the return value is typed as the protocol
510
513
  so no annotation is needed. Pass `obj_ref` (e.g. "$existing") to
511
514
  bind an existing variable instead of creating a new object.
515
+
516
+ A bare type literal such as ``make_proxy("", "[System.IO.Path]")``
517
+ yields a *static* proxy exposing the type's static methods,
518
+ properties and constants. `static=True` forces static mode for an
519
+ unbracketed type name or a ``$variable`` that holds a type object.
512
520
  """
513
521
  from .ps_proxy import PsProxy
514
522
 
@@ -520,9 +528,12 @@ class Shell:
520
528
  raise TypeError(
521
529
  f"{proto.__name__} carries no __ps_type_name__/__ps_expression__ "
522
530
  "metadata; regenerate it with generate_psobject")
523
- return PsProxy(self, ps_type, obj_ref if obj_ref is not None else expression)
531
+ is_static = static or bool(getattr(proto, "__ps_static__", False))
532
+ return PsProxy(self, ps_type, obj_ref if obj_ref is not None else expression,
533
+ static=is_static)
524
534
 
525
- return PsProxy(self, type_name, obj_ref if obj_ref is not None else "$obj")
535
+ return PsProxy(self, type_name, obj_ref if obj_ref is not None else "$obj",
536
+ static=static)
526
537
 
527
538
  def generate_psobject(self, command: str, output_path: Path) -> None:
528
539
  """Generate a PowerShell object from a command."""
@@ -7,6 +7,7 @@ import shutil
7
7
  import pytest
8
8
 
9
9
  from virtualshell.generate_psobject import (
10
+ _property_type,
10
11
  build_method_signatures,
11
12
  categorize_members,
12
13
  first_signature,
@@ -253,6 +254,38 @@ class TestRenderProtocol:
253
254
  def test_no_metadata_classvars_by_default(self):
254
255
  source = render_protocol("MyProxy", self.MEMBERS)
255
256
  assert "__ps_type_name__" not in source
257
+ assert "__ps_static__" not in source
258
+
259
+
260
+ class TestStaticRendering:
261
+ # Shaped like `[System.Math] | Get-Member -Static | ConvertTo-Json`.
262
+ MEMBERS = [
263
+ {"Name": "PI", "MemberType": 4,
264
+ "Definition": "static double PI {get;}"},
265
+ {"Name": "Sqrt", "MemberType": 64,
266
+ "Definition": "static double Sqrt(double d)"},
267
+ ]
268
+
269
+ def test_static_property_type_strips_prefix(self):
270
+ assert _property_type(
271
+ {"Definition": "static datetime Now {get;}"}) == "datetime"
272
+ # Instance definitions are unaffected.
273
+ assert _property_type({"Definition": "int Year {get;}"}) == "int"
274
+
275
+ def test_ps_static_classvar_embedded(self):
276
+ source = render_protocol(
277
+ "Math", self.MEMBERS,
278
+ ps_type_name="System.Math", ps_expression="[System.Math]",
279
+ ps_static=True)
280
+ compile(source, "<generated>", "exec")
281
+ assert "__ps_static__: ClassVar[bool] = True" in source
282
+ assert "__ps_expression__: ClassVar[str] = '[System.Math]'" in source
283
+
284
+ def test_static_members_render_with_real_types(self):
285
+ source = render_protocol("Math", self.MEMBERS, ps_static=True)
286
+ compile(source, "<generated>", "exec")
287
+ assert "def PI(self) -> float: ..." in source
288
+ assert "def Sqrt(self, d: float) -> float: ..." in source
256
289
 
257
290
 
258
291
  # =============================================================================
@@ -367,3 +400,37 @@ class TestGenerateEndToEnd:
367
400
  bound = shell.make_proxy(StringBuilder, "$vs_gen_bind")
368
401
  bound.Append("bundet")
369
402
  assert shell.run("$vs_gen_bind.ToString()").out.strip() == "bundet"
403
+
404
+ def test_generate_static_type(self, shell, tmp_path):
405
+ from virtualshell.generate_psobject import generate
406
+
407
+ out_file = tmp_path / "math_static.py"
408
+ generate(shell, "[System.Math]", out_file)
409
+ source = out_file.read_text(encoding="utf-8")
410
+
411
+ compile(source, str(out_file), "exec")
412
+ assert "class Math(Protocol):" in source
413
+ assert "__ps_static__: ClassVar[bool] = True" in source
414
+ assert "__ps_expression__: ClassVar[str] = '[System.Math]'" in source
415
+ assert "def Sqrt(self" in source
416
+ assert "def PI(self) -> float: ..." in source
417
+
418
+ def test_make_proxy_from_generated_static_protocol(self, shell, tmp_path):
419
+ """generate -> import -> make_proxy(StaticProtocol) round trip."""
420
+ import importlib.util
421
+
422
+ from virtualshell.generate_psobject import generate
423
+
424
+ out_file = tmp_path / "math_protocol.py"
425
+ generate(shell, "[System.Math]", out_file)
426
+
427
+ spec = importlib.util.spec_from_file_location("math_protocol", out_file)
428
+ module = importlib.util.module_from_spec(spec)
429
+ spec.loader.exec_module(module)
430
+ Math = module.Math
431
+
432
+ assert Math.__ps_static__ is True
433
+
434
+ m = shell.make_proxy(Math)
435
+ assert m.Sqrt(16.0) == 4.0
436
+ assert abs(m.PI - 3.141592653589793) < 1e-12
@@ -305,3 +305,93 @@ class TestMultiCall:
305
305
  al = shell.make_proxy("", "System.Collections.ArrayList")
306
306
  with pytest.raises(TypeError):
307
307
  al.proxy_multi_call(len, 3)
308
+
309
+
310
+ class TestStaticProxies:
311
+ def test_type_literal_selects_static_mode(self, shell):
312
+ m = shell.make_proxy("", "[System.Math]")
313
+ assert m.type_name == "System.Math"
314
+ assert "static" in repr(m)
315
+ assert m.Sqrt(16.0) == 4.0
316
+ assert m.Max(3, 7) == 7
317
+
318
+ def test_constant_field_reads_as_property(self, shell):
319
+ m = shell.make_proxy("", "[System.Math]")
320
+ assert abs(m.PI - 3.141592653589793) < 1e-12
321
+
322
+ def test_static_method_with_string_args(self, shell):
323
+ p = shell.make_proxy("", "[System.IO.Path]")
324
+ assert p.Combine("a", "b") in ("a\\b", "a/b")
325
+
326
+ def test_static_datetime_roundtrip(self, shell):
327
+ from datetime import datetime
328
+
329
+ dt = shell.make_proxy("", "[datetime]")
330
+ parsed = dt.Parse("2024-01-02T03:04:05")
331
+ assert parsed == datetime(2024, 1, 2, 3, 4, 5)
332
+
333
+ def test_enum_values_read_as_strings(self, shell):
334
+ dow = shell.make_proxy("", "[System.DayOfWeek]")
335
+ assert dow.Monday == "Monday"
336
+
337
+ def test_writable_static_property_and_const(self, shell):
338
+ shell.run(
339
+ "Add-Type -TypeDefinition 'public static class VsStaticTest { "
340
+ "public static int Counter { get; set; } "
341
+ "public static string Echo(string s) { return s + \"!\"; } "
342
+ "public const double Ratio = 2.5; }'",
343
+ raise_on_error=True)
344
+ t = shell.make_proxy("", "[VsStaticTest]")
345
+ t.Counter = 5
346
+ assert t.Counter == 5
347
+ assert t.Echo("hei") == "hei!"
348
+ assert t.Ratio == 2.5
349
+ with pytest.raises(AttributeError, match="read-only"):
350
+ t.Ratio = 3.0
351
+
352
+ def test_static_true_with_unbracketed_name(self, shell):
353
+ m = shell.make_proxy("", "System.Math", static=True)
354
+ assert m.Sqrt(9.0) == 3.0
355
+
356
+ def test_static_true_binds_existing_type_variable(self, shell):
357
+ shell.run("$vs_static_t = [System.Math]", raise_on_error=True)
358
+ m = shell.make_proxy("", "$vs_static_t", static=True)
359
+ assert m.Sqrt(25.0) == 5.0
360
+
361
+ def test_static_true_rejects_non_type_variable(self, shell):
362
+ shell.run("$vs_not_a_type = 42", raise_on_error=True)
363
+ with pytest.raises(ValueError, match="does not hold"):
364
+ shell.make_proxy("", "$vs_not_a_type", static=True)
365
+
366
+ def test_unknown_static_type_raises(self, shell):
367
+ with pytest.raises(RuntimeError, match="Failed to resolve static type"):
368
+ shell.make_proxy("", "[No.Such.Type.Exists]")
369
+
370
+ def test_instance_members_absent_from_static_schema(self, shell):
371
+ m = shell.make_proxy("", "[System.Text.StringBuilder]")
372
+ names = {x["Name"] for x in m.proxy_schema()["Methods"]}
373
+ # ToString is an instance method; a static proxy must not offer it.
374
+ assert "ToString" not in names
375
+
376
+ def test_static_and_instance_schemas_coexist(self, shell):
377
+ from virtualshell.ps_proxy import _SCHEMA_CACHE
378
+ shell.make_proxy("", "[System.Text.StringBuilder]")
379
+ shell.make_proxy("", "System.Text.StringBuilder")
380
+ run_id = shell.python_run_id
381
+ assert (run_id, "System.Text.StringBuilder") in _SCHEMA_CACHE
382
+ assert (run_id, "static:System.Text.StringBuilder") in _SCHEMA_CACHE
383
+
384
+ def test_static_multi_call(self, shell):
385
+ m = shell.make_proxy("", "[System.Math]")
386
+ assert m.proxy_multi_call(m.Sqrt, [4.0, 9.0, 16.0]) == [2.0, 3.0, 4.0]
387
+
388
+ def test_messagebox_scenario_schema(self, shell):
389
+ """The motivating case: a WinForms MessageBox static proxy exposes
390
+ Show without being called (calling it would block on a modal)."""
391
+ res = shell.run("Add-Type -AssemblyName System.Windows.Forms")
392
+ if not res.success:
393
+ pytest.skip("System.Windows.Forms not available")
394
+ mb = shell.make_proxy("", "[System.Windows.Forms.MessageBox]")
395
+ assert mb.type_name == "System.Windows.Forms.MessageBox"
396
+ names = {x["Name"] for x in mb.proxy_schema()["Methods"]}
397
+ assert "Show" in names
@@ -14,8 +14,11 @@ from virtualshell.ps_proxy import (
14
14
  _parse_iso_datetime,
15
15
  _parse_schema,
16
16
  _ps_quote,
17
+ _static_member_expr,
18
+ _static_method_call_expr,
17
19
  build_creation_strategies,
18
20
  split_invocation,
21
+ static_type_literal,
19
22
  )
20
23
 
21
24
 
@@ -49,6 +52,56 @@ class TestExpressions:
49
52
  assert expr == "$x.PSObject.Methods['my method'].Invoke(1, 2)"
50
53
 
51
54
 
55
+ class TestStaticExpressions:
56
+ def test_static_member(self):
57
+ assert _static_member_expr("$t", "PI") == "$t::PI"
58
+
59
+ def test_static_member_with_special_name_uses_reflection(self):
60
+ expr = _static_member_expr("$t", "weird name")
61
+ assert "InvokeMember('weird name'" in expr
62
+ assert "GetProperty,GetField" in expr
63
+
64
+ def test_static_call(self):
65
+ assert _static_method_call_expr("$t", "Sqrt", ["16"]) == "$t::Sqrt(16)"
66
+
67
+ def test_static_call_with_special_name_uses_reflection(self):
68
+ expr = _static_method_call_expr("$t", "my method", ["1", "2"])
69
+ assert "InvokeMember('my method'" in expr
70
+ assert "InvokeMethod" in expr
71
+ assert "@(1, 2)" in expr
72
+
73
+
74
+ class TestStaticTypeLiteral:
75
+ def test_bare_literal(self):
76
+ assert static_type_literal("[System.Math]") == "System.Math"
77
+
78
+ def test_whitespace_tolerated(self):
79
+ assert static_type_literal(" [System.IO.Path] ") == "System.IO.Path"
80
+
81
+ def test_generic_literal(self):
82
+ assert (static_type_literal("[System.Collections.Generic.List[int]]")
83
+ == "System.Collections.Generic.List[int]")
84
+
85
+ def test_plain_type_name_is_not_a_literal(self):
86
+ assert static_type_literal("System.Math") is None
87
+
88
+ def test_invocation_is_not_a_literal(self):
89
+ assert static_type_literal("[System.Text.StringBuilder]::new(3)") is None
90
+
91
+ def test_cast_is_not_a_literal(self):
92
+ assert static_type_literal("[int](3)") is None
93
+
94
+ def test_variable_is_not_a_literal(self):
95
+ assert static_type_literal("$var") is None
96
+
97
+ def test_unbalanced_brackets_rejected(self):
98
+ assert static_type_literal("[int][string]") is None
99
+
100
+ def test_empty_and_tiny_inputs(self):
101
+ assert static_type_literal("") is None
102
+ assert static_type_literal("[]") is None
103
+
104
+
52
105
  class TestSplitInvocation:
53
106
  def test_ctor_like(self):
54
107
  assert split_invocation("System.Text.StringBuilder(16)") == (
@@ -293,14 +293,17 @@ class TestConvenience:
293
293
  created = {}
294
294
 
295
295
  class StubProxy:
296
- def __init__(self, shell, type_name, object_ref):
297
- created["args"] = (shell, type_name, object_ref)
296
+ def __init__(self, shell, type_name, object_ref, *, static=False):
297
+ created["args"] = (shell, type_name, object_ref, static)
298
298
 
299
299
  monkeypatch.setattr(ps_proxy_module, "PsProxy", StubProxy)
300
300
  sh = Shell(cpp_module=fake_core).start()
301
301
  proxy = sh.make_proxy("System.IO.FileInfo", "$file")
302
302
  assert isinstance(proxy, StubProxy)
303
- assert created["args"] == (sh, "System.IO.FileInfo", "$file")
303
+ assert created["args"] == (sh, "System.IO.FileInfo", "$file", False)
304
+
305
+ sh.make_proxy("System.Math", "$t", static=True)
306
+ assert created["args"] == (sh, "System.Math", "$t", True)
304
307
 
305
308
  def test_make_proxy_accepts_generated_protocol_class(self, fake_core, monkeypatch):
306
309
  import virtualshell.ps_proxy as ps_proxy_module
@@ -308,8 +311,8 @@ class TestConvenience:
308
311
  created = {}
309
312
 
310
313
  class StubProxy:
311
- def __init__(self, shell, type_name, object_ref):
312
- created["args"] = (type_name, object_ref)
314
+ def __init__(self, shell, type_name, object_ref, *, static=False):
315
+ created["args"] = (type_name, object_ref, static)
313
316
 
314
317
  class StringBuilder: # shaped like generate_psobject output
315
318
  __ps_type_name__ = "System.Text.StringBuilder"
@@ -321,11 +324,35 @@ class TestConvenience:
321
324
  # Default: recreate the object from the embedded expression.
322
325
  sh.make_proxy(StringBuilder)
323
326
  assert created["args"] == ("System.Text.StringBuilder",
324
- "[System.Text.StringBuilder]::new()")
327
+ "[System.Text.StringBuilder]::new()", False)
325
328
 
326
329
  # Explicit obj_ref binds an existing variable instead.
327
330
  sh.make_proxy(StringBuilder, "$existing")
328
- assert created["args"] == ("System.Text.StringBuilder", "$existing")
331
+ assert created["args"] == ("System.Text.StringBuilder", "$existing", False)
332
+
333
+ def test_make_proxy_honours_static_protocol_metadata(self, fake_core, monkeypatch):
334
+ import virtualshell.ps_proxy as ps_proxy_module
335
+
336
+ created = {}
337
+
338
+ class StubProxy:
339
+ def __init__(self, shell, type_name, object_ref, *, static=False):
340
+ created["args"] = (type_name, object_ref, static)
341
+
342
+ class Math: # shaped like generate_psobject output for '[System.Math]'
343
+ __ps_type_name__ = "System.Math"
344
+ __ps_expression__ = "[System.Math]"
345
+ __ps_static__ = True
346
+
347
+ monkeypatch.setattr(ps_proxy_module, "PsProxy", StubProxy)
348
+ sh = Shell(cpp_module=fake_core).start()
349
+
350
+ sh.make_proxy(Math)
351
+ assert created["args"] == ("System.Math", "[System.Math]", True)
352
+
353
+ # Static metadata sticks when binding an existing type variable.
354
+ sh.make_proxy(Math, "$existing_type")
355
+ assert created["args"] == ("System.Math", "$existing_type", True)
329
356
 
330
357
  def test_make_proxy_rejects_class_without_metadata(self, fake_core):
331
358
  class Naked:
@@ -18,7 +18,7 @@
18
18
  - `pwsh(s: str, timeout: float|None=None, raise_on_error=False) -> ExecutionResult` _(executes a **literal** string safely)_
19
19
  - `save_session(timeout: float|None=None, raise_on_error=True) -> ExecutionResult`
20
20
  - `restore_session(snapshot_path: str|Path, timeout: float|None=None, raise_on_error=True) -> ExecutionResult`
21
- - `make_proxy(type_name: str, obj_ref: str = "$obj") -> PsProxy` _(live object proxy; `obj_ref` is a `$variable` or a creation expression)_
21
+ - `make_proxy(type_name: str, obj_ref: str = "$obj", *, static: bool = False) -> PsProxy` _(live object proxy; `obj_ref` is a `$variable`, a creation expression, or a `[Type]` literal for a static proxy)_
22
22
  - `generate_psobject(type_expression: str, output_path: str|Path) -> None`
23
23
  - `zero_copy_bridge(frame_mb: int = 16, chunk_mb: int = 4) -> ZeroCopyBridge` _(shared per-shell bridge, created lazily)_
24
24
 
@@ -57,6 +57,26 @@ metadata, which is what lets `make_proxy(WebClient)` recreate the object
57
57
  without repeating the PowerShell expression. Pass an explicit variable to
58
58
  bind instead: `shell.make_proxy(WebClient, "$client")`.
59
59
 
60
+ ## Static Classes
61
+
62
+ Passing a bare `[Type]` literal generates a protocol for the type's
63
+ **static** surface instead (collected with `Get-Member -Static`):
64
+
65
+ ```python
66
+ shell.generate_psobject("[System.Math]", Path("Math.py"))
67
+ ```
68
+
69
+ The generated class additionally embeds ``__ps_static__ = True``, so
70
+ `make_proxy(Math)` binds a [static proxy](make_proxy.md#static-classes)
71
+ automatically:
72
+
73
+ ```python
74
+ from Math import Math
75
+
76
+ math = shell.make_proxy(Math)
77
+ math.Sqrt(16.0) # 4.0 - with full IDE completion
78
+ ```
79
+
60
80
  The protocol only describes the members discovered at generation time. If the PowerShell type changes or you need a different view, rerun `generate_psobject` with the updated command.
61
81
 
62
82
  ### End-to-End Example
@@ -7,14 +7,15 @@ The proxy is implemented in pure Python and works on Windows, Linux and macOS. B
7
7
  ## Signature
8
8
 
9
9
  ```python
10
- proxy = shell.make_proxy(type_name: str, obj_ref: str | None = None)
11
- proxy = shell.make_proxy(GeneratedProtocol, obj_ref: str | None = None)
10
+ proxy = shell.make_proxy(type_name: str, obj_ref: str | None = None, *, static: bool = False)
11
+ proxy = shell.make_proxy(GeneratedProtocol, obj_ref: str | None = None, *, static: bool = False)
12
12
  ```
13
13
 
14
14
  | Parameter | Description |
15
15
  |-----------|-------------|
16
16
  | `type_name` | Optional label (`""` is fine — the real type is resolved from the live object), **or** a Protocol class produced by `generate_psobject`. |
17
- | `obj_ref` | Either an existing PowerShell variable (`"$client"`), or a **creation expression** such as `"System.Text.StringBuilder(32)"`. Creation tries `[Type]::new(...)`, `New-Object`, and COM instantiation in order. |
17
+ | `obj_ref` | Either an existing PowerShell variable (`"$client"`), or a **creation expression** such as `"System.Text.StringBuilder(32)"`. Creation tries `[Type]::new(...)`, `New-Object`, and COM instantiation in order. A bare type literal (`"[System.IO.Path]"`) selects [static mode](#static-classes). |
18
+ | `static` | Force static mode for an unbracketed type name or a `$variable` that holds a type object. |
18
19
 
19
20
  With a generated Protocol class as the first argument, the object is created
20
21
  from the metadata embedded in the class, and the return value is typed as the
@@ -49,6 +50,45 @@ with Shell() as sh:
49
50
  client = sh.make_proxy("", "$client")
50
51
  ```
51
52
 
53
+ ## Static Classes
54
+
55
+ A bare `[Type]` literal binds the *type itself* instead of creating an
56
+ instance. The proxy then exposes the type's **static** methods, properties
57
+ and constants (invoked with `::` in the session):
58
+
59
+ ```python
60
+ with Shell() as sh:
61
+ math = sh.make_proxy("", "[System.Math]")
62
+ math.Sqrt(16.0) # 4.0
63
+ math.PI # 3.141592653589793 (const field)
64
+
65
+ path = sh.make_proxy("", "[System.IO.Path]")
66
+ path.Combine("a", "b") # "a\\b"
67
+
68
+ dow = sh.make_proxy("", "[System.DayOfWeek]")
69
+ dow.Monday # "Monday" (enum values are fields)
70
+ ```
71
+
72
+ The motivating example — a WinForms message box:
73
+
74
+ ```python
75
+ with Shell(initial_commands=["Add-Type -AssemblyName System.Windows.Forms"]) as sh:
76
+ msg_box = sh.make_proxy("", "[System.Windows.Forms.MessageBox]")
77
+ msg_box.Show("Hello from Python!", "Title") # blocks until dismissed
78
+ ```
79
+
80
+ Notes:
81
+
82
+ - Return values convert exactly like instance calls (the table below):
83
+ scalars inline, `byte[]` via the bridge, complex values as sub-proxies.
84
+ - Writable static properties can be assigned; consts and `readonly` fields
85
+ are read-only and raise `AttributeError` on assignment.
86
+ - Instance members are **not** available on a static proxy (and vice
87
+ versa); the same type can have one proxy of each kind.
88
+ - `static=True` forces static mode when the name is unbracketed
89
+ (`make_proxy("", "System.Math", static=True)`) or when binding a
90
+ `$variable` that already holds a type object.
91
+
52
92
  ## Value Conversion
53
93
 
54
94
  Every read is routed through a runtime type check in PowerShell:
@@ -1 +0,0 @@
1
- version = "1.2.0"
File without changes
File without changes
File without changes