Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ __pycache__

/tests/out/
/pytest_args.txt
/venv*

# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,python,c++
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,python,c++
Expand Down
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "PyRx Remote Attach",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "127.0.0.1",
"port": 5678
}
}
]
}
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = ["wxpython>=4.2.2", "pywin32", "debugpy>=1.8.0"]

[project.optional-dependencies]
dev = ["pytest", "build"]
ptrepl = ["ptpython", "prompt_toolkit"]

[project.urls]
Homepage = "https://pyarx.blogspot.com/"
Expand Down
37 changes: 30 additions & 7 deletions pyrx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@
import PyBr as Br # isort: skip # type: ignore
import PyAx as Ax # isort: skip # type: ignore

if importlib.util.find_spec("PyBrxCv") is not None:
import PyBrxCv as Cv # isort: skip # type: ignore
if importlib.util.find_spec("PyBrxBim") is not None:
import PyBrxBim as Bim # isort: skip # type: ignore
if importlib.util.find_spec("PyBrx") is not None:
import PyBrx as Brx # isort: skip # type: ignore
try:
import PyBrxCv as Cv
except ModuleNotFoundError:
Cv = None
try:
import PyBrxBim as Bim
except ModuleNotFoundError:
Bim = None
try:
import PyBrx as Brx
except ModuleNotFoundError:
Brx = None

except ModuleNotFoundError:
warnings.warn("PyRx modules are not available, they must be invoked from a CAD application.")
Expand Down Expand Up @@ -51,4 +57,21 @@
from .commands import command
from .utils.reload import reload

__all__ = ("Ap", "Br", "Db", "Ed", "Ge", "Gi", "Gs", "Pl", "Rx", "Sm", "Ax", "command", "reload")
__all__ = (
"Ap",
"Ax",
"Bim",
"Br",
"Brx",
"Cv",
"Db",
"Ed",
"Ge",
"Gi",
"Gs",
"Pl",
"Rx",
"Sm",
"command",
"reload",
)
37 changes: 34 additions & 3 deletions pyrx/_host_init.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
from __future__ import annotations

from pyrx import Rx, Ap, Ed
def PyRxCmd_hostinit():
print("Hello world!")
import threading

from pyrx import command, reload
from pyrx.PyRxDebug import startListener
from pyrx.repl.stdlibrepl import run_stdlib_repl

reload("pyrx")


command(startListener, name="PYDEBUG")


@command
def pystdrepl():
thread = threading.Thread(target=run_stdlib_repl)
thread.start()


try:
from pyrx.repl.ptrepl import run_ptpython_repl
except ImportError:
import importlib.util

if importlib.util.find_spec("ptpython") is not None:
raise

@command
def pyptrepl():
print("\nptpython is not installed. Please run `pip install ptpython`.\n")
else:

@command
def pyptrepl():
thread = threading.Thread(target=run_ptpython_repl)
thread.start()
2 changes: 1 addition & 1 deletion pyrx/ap/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def wrapper():
Ap.Application.removeOnIdleWinMsg(wrapper)
try:
res = func(*args, **kwargs)
except Exception as e:
except BaseException as e:
q.put_nowait((False, e))
else:
q.put_nowait((True, res))
Expand Down
2 changes: 1 addition & 1 deletion pyrx/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def console(allow_existing=True, stdout_redirect=True, stderr_redirect=True, std
_redirect_stdin(conin),
):
try:
yield
yield conin, conout
finally:
if not console_exists:
print("You can close the console . . .", file=conout, flush=True)
Expand Down
Empty file added pyrx/repl/__init__.py
Empty file.
112 changes: 112 additions & 0 deletions pyrx/repl/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from __future__ import annotations

import abc
import sys
import typing as t
from contextlib import contextmanager

from pyrx import Db, Ed
from pyrx.ap.utils import call_after, call_in_main_thread # noqa
from pyrx.console import redirect_stderr, redirect_stdin, redirect_stdout

# Is it possible to handle the console close signal and prevent the host from closing?
WARNING_CLOSE_CONSOLE = (
"**********************************************************************\n"
"* Warning: Do not close the console window, this will immediately *\n"
"* close the host application. First exit the REPL. *\n"
"**********************************************************************"
)


class ReplMixin(abc.ABC):
def __init__(
self,
stdin: t.TextIO | None = None,
stdout: t.TextIO | None = None,
stderr: t.TextIO | None = None,
) -> None:
self._stdin = stdin
self._stdout = stdout
self._stderr = stderr

@property
def stdin(self) -> t.TextIO | None:
return self._stdin or sys.stdin

@stdin.setter
def stdin(self, value: t.TextIO | None) -> None:
self._stdin = value

@property
def stdout(self) -> t.TextIO | None:
return self._stdout or sys.stdout

@stdout.setter
def stdout(self, value: t.TextIO | None) -> None:
self._stdout = value

@property
def stderr(self) -> t.TextIO | None:
return self._stderr or sys.stderr

@stderr.setter
def stderr(self, value: t.TextIO | None) -> None:
self._stderr = value

@contextmanager
def redirect(self):
with (
redirect_stdin(self.stdin),
redirect_stdout(self.stderr),
redirect_stderr(self.stdout),
):
yield

@property
def default_namespace(self):
import builtins

from pyrx import Ap, Ax, Bim, Br, Brx, Cv, Db, Ed, Ge, Gi, Gs, Pl, Rx, Sm

return {
"__name__": "__main__",
"__package__": None,
"__doc__": None,
"__builtins__": builtins,
"Ap": Ap,
"Ax": Ax,
"Bim": Bim,
"Br": Br,
"Brx": Brx,
"Cv": Cv,
"Db": Db,
"Ed": Ed,
"Ge": Ge,
"Gi": Gi,
"Gs": Gs,
"Pl": Pl,
"Rx": Rx,
"Sm": Sm,
"curdb": curdb,
"entsel": entsel,
"select": select,
}


# shortcuts for the REPL

curdb = Db.curDb


def entsel() -> None | Db.ObjectId:
status, id_, _ = Ed.Editor.entSel("Select: ")
if not status == Ed.PromptStatus.eOk:
return None
return id_


def select() -> None | list[Db.ObjectId]:
status, sset = Ed.Editor.select()
if not status == Ed.PromptStatus.eOk:
return None
return sset.objectIds()
102 changes: 102 additions & 0 deletions pyrx/repl/ptrepl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from __future__ import annotations

import os
import sys
import types
import typing as t

from prompt_toolkit.input import create_input
from prompt_toolkit.output import create_output
from ptpython.repl import PythonRepl

from pyrx.console import console

from .base import WARNING_CLOSE_CONSOLE, ReplMixin, call_in_main_thread, redirect_stdin


class PtPythonRepl(ReplMixin, PythonRepl):
def __init__(
self,
*,
stdin=None,
stdout=None,
stderr=None,
input=None,
output=None,
globals=None,
locals=None,
**kwargs,
) -> None:
if globals is None:
globals = self.default_namespace

locals = locals or globals

def get_globals():
return globals

def get_locals():
return locals

ReplMixin.__init__(self, stdin=stdin, stdout=stdout, stderr=stderr)
PythonRepl.__init__(
self,
get_globals=get_globals,
get_locals=get_locals,
input=input,
output=output,
**kwargs,
)

@call_in_main_thread
def _eval(self, code: types.CodeType) -> None:
with self.redirect():
return eval(code, self.get_globals(), self.get_locals())

# override PythonRepl.eval
def eval(self, line: str) -> t.Any:
# WORKAROUND: Due to a bug in Jedi, the current directory is removed
# from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
if "" not in sys.path:
sys.path.insert(0, "")

if line.lstrip().startswith("!"):
# Run as shell command
os.system(line[1:])
else:
# Try eval first
try:
code = self._compile_with_flags(line, "eval")
except SyntaxError:
pass
else:
# No syntax errors for eval. Do eval.
result = self._eval(code) # override !

self._store_eval_result(result)
return result

# If not a valid `eval` expression, run using `exec` instead.
# Note that we shouldn't run this in the `except SyntaxError` block
# above, then `sys.exc_info()` would not report the right error.
# See issue: https://github.com/prompt-toolkit/ptpython/issues/435
code = self._compile_with_flags(line, "exec")
result = self._eval(code) # override !

return None


def run_ptpython_repl():
with console(
stdin_redirect=False,
stdout_redirect=False,
stderr_redirect=False,
) as (conin, conout):
print(WARNING_CLOSE_CONSOLE, file=conout)
con_output = create_output(conout)
with redirect_stdin(conin):
con_input = create_input()
repl = PtPythonRepl(
stdin=conin, stdout=conout, stderr=conout, input=con_input, output=con_output
)
repl.run()
Loading