Skip to content

Commit e5502f4

Browse files
committed
powershell completion tests almost working #156
1 parent 53d46d7 commit e5502f4

File tree

6 files changed

+164
-161
lines changed

6 files changed

+164
-161
lines changed

django_typer/templates/shell_complete/powershell.ps1

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ $scriptblock = {
2020
$pythonPathOption = "--pythonpath=$($matches[1])"
2121
}
2222

23-
{{ manage_script_name }} {{ django_command }} $settingsOption $pythonPathOption --shell {{ shell }} {{ color }} complete "$($commandText)" | ForEach-Object {
23+
$results = {{ manage_script_name }} {{ django_command }} $settingsOption $pythonPathOption --shell {{ shell }} {{ color }} complete "$($commandText)"
24+
25+
if ($results.Count -eq 0) {
26+
# avoid default path completion
27+
return $null
28+
}
29+
30+
$results | ForEach-Object {
2431
$commandArray = $_ -Split ":::"
2532
$type = $commandArray[0]
2633
$value = $commandArray[1]

django_typer/utils.py

+29-33
Original file line numberDiff line numberDiff line change
@@ -236,36 +236,32 @@ def get_win_shell() -> str:
236236
from shellingham import ShellDetectionFailure
237237

238238
assert platform.system() == "Windows"
239-
pwsh = shutil.which("pwsh")
240-
powershell = shutil.which("powershell")
241-
if pwsh and not powershell:
242-
return "pwsh"
243-
elif powershell and not pwsh:
244-
return "powershell"
245-
try:
246-
ps_command = """
247-
$parent = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = {pid}";
248-
$parentPid = $parent.ParentProcessId;
249-
$parentInfo = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = $parentPid";
250-
$parentInfo | Select-Object Name, ProcessId | ConvertTo-Json -Depth 1
251-
"""
252-
pid = os.getpid()
253-
while True:
254-
result = subprocess.run(
255-
["pwsh", "-NoProfile", "-Command", ps_command.format(pid=pid)],
256-
capture_output=True,
257-
text=True,
258-
).stdout.strip()
259-
if not result:
260-
break
261-
process = json.loads(result)
262-
if "pwsh" in process.get("Name", ""):
263-
return "pwsh"
264-
elif "powershell" in process.get("Name", ""):
265-
return "powershell"
266-
pid = process["ProcessId"]
267-
268-
raise ShellDetectionFailure("Unable to detect windows shell")
269-
270-
except Exception as e:
271-
raise ShellDetectionFailure("Unable to detect windows shell") from e
239+
pwsh = shutil.which("pwsh") or shutil.which("powershell")
240+
if pwsh:
241+
try:
242+
ps_command = """
243+
$parent = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = {pid}";
244+
$parentPid = $parent.ParentProcessId;
245+
$parentInfo = Get-CimInstance -Query "SELECT * FROM Win32_Process WHERE ProcessId = $parentPid";
246+
$parentInfo | Select-Object Name, ProcessId | ConvertTo-Json -Depth 1
247+
"""
248+
pid = os.getpid()
249+
while True:
250+
result = subprocess.run(
251+
[pwsh, "-NoProfile", "-Command", ps_command.format(pid=pid)],
252+
capture_output=True,
253+
text=True,
254+
).stdout.strip()
255+
if not result:
256+
break
257+
process = json.loads(result)
258+
if "pwsh" in process.get("Name", ""):
259+
return "pwsh"
260+
elif "powershell" in process.get("Name", ""):
261+
return "powershell"
262+
pid = process["ProcessId"]
263+
264+
except Exception as e:
265+
raise ShellDetectionFailure("Unable to detect windows shell") from e
266+
267+
raise ShellDetectionFailure("Unable to detect windows shell")

tests/shellcompletion/__init__.py

+106-70
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import fcntl
21
import os
3-
import pty
42
import re
53
import select
64
import struct
75
import subprocess
86
import sys
9-
import termios
107
import time
118
import typing as t
129
from pathlib import Path
1310
import pytest
1411
from functools import cached_property
1512
import re
1613
import subprocess
14+
import platform
1715

1816
from shellingham import detect_shell
1917

@@ -67,11 +65,9 @@ class _DefaultCompleteTestCase(with_typehint(TestCase)):
6765
manage_script = "manage.py"
6866
launch_script = "./manage.py"
6967

70-
@property
71-
def interactive_opt(self):
72-
# currently all supported shells support -i for interactive mode
73-
# this includes zsh, bash, fish and powershell
74-
return "-i"
68+
interactive_opt: t.Optional[str] = None
69+
70+
environment: t.List[str] = []
7571

7672
@cached_property
7773
def command(self) -> ShellCompletion:
@@ -116,75 +112,109 @@ def remove(self, script=None):
116112
self.get_completions("ping") # just to reinit shell
117113
self.verify_remove(script=script)
118114

119-
def set_environment(self, fd):
120-
os.write(fd, f"PATH={Path(sys.executable).parent}:$PATH\n".encode())
121-
os.write(
122-
fd,
123-
f"DJANGO_SETTINGS_MODULE=tests.settings.completion\n".encode(),
124-
)
115+
if platform.system() == "Windows":
125116

126-
def get_completions(self, *cmds: str, scrub_output=True) -> str:
127-
def read(fd):
128-
"""Function to read from a file descriptor."""
129-
return os.read(fd, 1024 * 1024).decode()
130-
131-
# Create a pseudo-terminal
132-
master_fd, slave_fd = pty.openpty()
133-
134-
# Define window size - width and height
135-
os.set_blocking(slave_fd, False)
136-
win_size = struct.pack("HHHH", 24, 80, 0, 0) # 24 rows, 80 columns
137-
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, win_size)
138-
139-
# env = os.environ.copy()
140-
# env["TERM"] = "xterm-256color"
141-
142-
# Spawn a new shell process
143-
shell = self.shell or detect_shell()[0]
144-
process = subprocess.Popen(
145-
[shell, *([self.interactive_opt] if self.interactive_opt else [])],
146-
stdin=slave_fd,
147-
stdout=slave_fd,
148-
stderr=slave_fd,
149-
text=True,
150-
# env=env,
151-
# preexec_fn=os.setsid,
152-
)
153-
# Wait for the shell to start and get to the prompt
154-
print(read(master_fd))
117+
def get_completions(self, *cmds: str, scrub_output=True) -> str:
118+
import winpty
119+
120+
assert self.shell
121+
122+
pty = winpty.PTY(24, 80)
123+
124+
def read_all() -> str:
125+
output = ""
126+
while data := pty.read():
127+
output += data
128+
time.sleep(0.1)
129+
return output
130+
131+
# Start the subprocess
132+
pty.spawn(
133+
self.shell, *([self.interactive_opt] if self.interactive_opt else [])
134+
)
135+
136+
# Wait for the shell to start and get to the prompt
137+
time.sleep(3)
138+
read_all()
139+
140+
for line in self.environment:
141+
pty.write(line)
142+
143+
time.sleep(2)
144+
output = read_all() + read_all()
145+
146+
pty.write(" ".join(cmds))
147+
time.sleep(0.1)
148+
pty.write("\t")
149+
150+
time.sleep(2)
151+
completion = read_all() + read_all()
155152

156-
self.set_environment(master_fd)
153+
return scrub(completion) if scrub_output else completion
157154

158-
print(read(master_fd))
159-
# Send a command with a tab character for completion
155+
else:
160156

161-
cmd = " ".join(cmds)
162-
os.write(master_fd, cmd.encode())
163-
time.sleep(0.25)
157+
def get_completions(self, *cmds: str, scrub_output=True) -> str:
158+
import fcntl
159+
import termios
160+
import pty
164161

165-
print(f'"{cmd}"')
166-
os.write(master_fd, b"\t\t\t")
162+
def read(fd):
163+
"""Function to read from a file descriptor."""
164+
return os.read(fd, 1024 * 1024).decode()
167165

168-
time.sleep(0.25)
166+
# Create a pseudo-terminal
167+
master_fd, slave_fd = pty.openpty()
169168

170-
# Read the output
171-
output = read_all_from_fd_with_timeout(master_fd)
169+
# Define window size - width and height
170+
os.set_blocking(slave_fd, False)
171+
win_size = struct.pack("HHHH", 24, 80, 0, 0) # 24 rows, 80 columns
172+
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, win_size)
172173

173-
# todo - avoid large output because this can mess things up
174-
if "do you wish" in output or "Display all" in output:
175-
os.write(master_fd, b"y\n")
174+
# env = os.environ.copy()
175+
# env["TERM"] = "xterm-256color"
176+
177+
# Spawn a new shell process
178+
shell = self.shell or detect_shell()[0]
179+
process = subprocess.Popen(
180+
[shell, *([self.interactive_opt] if self.interactive_opt else [])],
181+
stdin=slave_fd,
182+
stdout=slave_fd,
183+
stderr=slave_fd,
184+
text=True,
185+
# env=env,
186+
# preexec_fn=os.setsid,
187+
)
188+
# Wait for the shell to start and get to the prompt
189+
print(read(master_fd))
190+
191+
for line in self.environment:
192+
os.write(master_fd, line.encode())
193+
194+
print(read(master_fd))
195+
# Send a command with a tab character for completion
196+
197+
cmd = " ".join(cmds)
198+
os.write(master_fd, cmd.encode())
176199
time.sleep(0.25)
200+
201+
print(f'"{cmd}"')
202+
os.write(master_fd, b"\t\t\t")
203+
204+
time.sleep(0.25)
205+
206+
# Read the output
177207
output = read_all_from_fd_with_timeout(master_fd)
178208

179-
# Clean up
180-
os.close(slave_fd)
181-
os.close(master_fd)
182-
process.terminate()
183-
process.wait()
184-
# remove bell character which can show up in some terminals where we hit tab
185-
if scrub_output:
186-
return scrub(output)
187-
return output
209+
# Clean up
210+
os.close(slave_fd)
211+
os.close(master_fd)
212+
process.terminate()
213+
process.wait()
214+
# remove bell character which can show up in some terminals where we hit tab
215+
if scrub_output:
216+
return scrub(output)
217+
return output
188218

189219
def run_app_completion(self):
190220
completions = self.get_completions(self.launch_script, "completion", " ")
@@ -323,18 +353,24 @@ class _InstalledScriptTestCase(_DefaultCompleteTestCase):
323353
manage_script = "django_manage"
324354
launch_script = "django_manage"
325355

326-
def setUp(self):
356+
@classmethod
357+
def setUpClass(cls):
327358
lines = []
328-
with open(self.MANAGE_SCRIPT_TMPL, "r") as f:
359+
with open(cls.MANAGE_SCRIPT_TMPL, "r") as f:
329360
for line in f.readlines():
330361
if line.startswith("#!{{shebang}}"):
331362
line = f"#!{sys.executable}\n"
332363
lines.append(line)
333-
exe = Path(sys.executable).parent / self.manage_script
364+
exe = Path(sys.executable).parent / cls.manage_script
334365
with open(exe, "w") as f:
335366
for line in lines:
336367
f.write(line)
337368

338369
# make the script executable
339370
os.chmod(exe, os.stat(exe).st_mode | 0o111)
340-
super().setUp()
371+
372+
if platform.system() == "Windows":
373+
with open(exe.with_suffix(".cmd"), "w") as f:
374+
f.write(f'@echo off{os.linesep}"{sys.executable}" "%~dp0{exe.name}" %*')
375+
os.chmod(exe, os.stat(exe.with_suffix(".cmd")).st_mode | 0o111)
376+
super().setUpClass()

tests/shellcompletion/test_bash.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@
1313
class BashTests(_DefaultCompleteTestCase, TestCase):
1414
shell = "bash"
1515
directory = Path("~/.bash_completions").expanduser()
16+
interactive_opt = "-i"
1617

17-
def set_environment(self, fd):
18-
os.write(
19-
fd,
20-
f"export DJANGO_SETTINGS_MODULE=tests.settings.completion\n".encode(),
21-
)
22-
os.write(fd, "source ~/.bashrc\n".encode())
23-
activate = Path(sys.executable).absolute().parent / "activate"
24-
if activate.is_file():
25-
os.write(fd, f"source {activate}\n".encode())
18+
environment = [
19+
f"export DJANGO_SETTINGS_MODULE=tests.settings.completion{os.linesep}",
20+
f"source ~/.bashrc{os.linesep}",
21+
f"source {Path(sys.executable).absolute().parent / 'activate'}{os.linesep}",
22+
]
2623

2724
def verify_install(self, script=None):
2825
if not script:

0 commit comments

Comments
 (0)