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.
- {virtualshell-1.2.0 → virtualshell-1.2.1}/PKG-INFO +10 -1
- {virtualshell-1.2.0 → virtualshell-1.2.1}/README.md +9 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/binder.cpp +1 -1
- {virtualshell-1.2.0 → virtualshell-1.2.1}/pyproject.toml +1 -1
- virtualshell-1.2.1/src/virtualshell/_version.py +1 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/generate_psobject.py +62 -15
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/ps_proxy.py +173 -8
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/shell.py +16 -5
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_generate_psobject.py +67 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_ps_proxy_integration.py +90 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_ps_proxy_unit.py +53 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_shell_unit.py +34 -7
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/API Overview.md +1 -1
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/generate_psobject.md +20 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/make_proxy.md +43 -3
- virtualshell-1.2.0/src/virtualshell/_version.py +0 -1
- {virtualshell-1.2.0 → virtualshell-1.2.1}/.github/workflows/test_build-only.yml +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/.github/workflows/workflow.yml +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/.gitignore +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/CMakeLists.txt +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/CMakeSettings.json +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/LICENSE +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/bench.csv +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/bench.json +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/vs_bench.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/zcb_bench.json +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/bench/zcb_bench.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/cmd_state.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/config.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/dev_debug.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/execution_result.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/helpers.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/io_pump.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/powershell_process.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/process.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/py_bridge.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/timeout_watcher.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/include/virtual_shell.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/io_pump.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/powershell_process.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/src/virtual_shell.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/CMakeLists.txt +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_framework.hpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_helpers.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_main.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/cpp/tests/test_virtual_shell.cpp +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/__init__.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_bootstrap.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_module.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/_protocols.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/errors.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/get-session.ps1 +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/ps_object.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/save-session.ps1 +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/zero_copy_bridge.ps1 +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/src/virtualshell/zero_copy_bridge_shell.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/conftest.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_integration.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_psobject_unit.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_utils_unit.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_zcb_channel_unit.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/tests/test_zcb_integration.py +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Getting started.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Installation.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Getting started/Quickstart.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Help/FAQ.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Help/Troubleshooting.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Home.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Benchmarks.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Changelog.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Project/Design & Architecture.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Asynchronous Execution.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Configuration.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Error Handling.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Performance Tips.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Running Scripts.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Security Notes.md +0 -0
- {virtualshell-1.2.0 → virtualshell-1.2.1}/wiki/Usage/Synchronous Execution.md +0 -0
- {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.
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
|
|
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 = ""
|
|
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
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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"{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|