robotcode-plugin 2.7.0__tar.gz → 2.8.0.dev8__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 (17) hide show
  1. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/.gitignore +4 -1
  2. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/PKG-INFO +3 -2
  3. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/pyproject.toml +1 -0
  4. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/__init__.py +23 -1
  5. robotcode_plugin-2.8.0.dev8/src/robotcode/plugin/__version__.py +1 -0
  6. robotcode_plugin-2.8.0.dev8/src/robotcode/plugin/click_helper/exit_codes.py +21 -0
  7. robotcode_plugin-2.8.0.dev8/src/robotcode/plugin/click_helper/wrappable.py +110 -0
  8. robotcode_plugin-2.7.0/src/robotcode/plugin/__version__.py +0 -1
  9. robotcode_plugin-2.7.0/src/robotcode/plugin/click_helper/wrappable.py +0 -26
  10. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/README.md +0 -0
  11. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/_agent_detection.py +0 -0
  12. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/click_helper/aliases.py +0 -0
  13. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/click_helper/options.py +0 -0
  14. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/click_helper/types.py +0 -0
  15. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/manager.py +0 -0
  16. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/py.typed +0 -0
  17. {robotcode_plugin-2.7.0 → robotcode_plugin-2.8.0.dev8}/src/robotcode/plugin/specs.py +0 -0
@@ -279,6 +279,9 @@ dist
279
279
  # vuepress build output
280
280
  .vuepress/dist
281
281
 
282
+ # Astro (documentation site)
283
+ docs/.astro/
284
+
282
285
  # Serverless directories
283
286
  .serverless/
284
287
 
@@ -339,7 +342,7 @@ bundled/libs
339
342
  # .agents
340
343
  .agents/
341
344
  skills-lock.json
342
- .claude
345
+ #.claude
343
346
 
344
347
  # sarif files
345
348
  /**/*.sarif.json
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: robotcode-plugin
3
- Version: 2.7.0
3
+ Version: 2.8.0-dev.8
4
4
  Summary: Some classes for RobotCode plugin management
5
5
  Project-URL: Donate, https://opencollective.com/robotcode
6
6
  Project-URL: Documentation, https://github.com/robotcodedev/robotcode#readme
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
22
23
  Classifier: Programming Language :: Python :: Implementation :: CPython
23
24
  Classifier: Programming Language :: Python :: Implementation :: PyPy
24
25
  Classifier: Topic :: Utilities
@@ -18,6 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.12",
19
19
  "Programming Language :: Python :: 3.13",
20
20
  "Programming Language :: Python :: 3.14",
21
+ "Programming Language :: Python :: 3.15",
21
22
  "Programming Language :: Python :: Implementation :: CPython",
22
23
  "Programming Language :: Python :: Implementation :: PyPy",
23
24
  "Operating System :: OS Independent",
@@ -83,6 +83,8 @@ class CommonConfig:
83
83
  colored_output: ColoredOutput = ColoredOutput.AUTO
84
84
  default_paths: Optional[Sequence[str]] = None
85
85
  launcher_script: Optional[str] = None
86
+ wrapper: Optional[str] = None
87
+ no_wrapper: bool = False
86
88
  output_format: Optional[OutputFormat] = None
87
89
  pager: Optional[bool] = None
88
90
  log_enabled: bool = False
@@ -136,6 +138,7 @@ def _get_deep_markdown_cls() -> "type[Markdown]":
136
138
  TableHeaderElement,
137
139
  TableRowElement,
138
140
  )
141
+ from rich.segment import Segment
139
142
  from rich.text import Text
140
143
 
141
144
  class LeftHeading(Heading):
@@ -196,6 +199,25 @@ def _get_deep_markdown_cls() -> "type[Markdown]":
196
199
  parser.options.maxNesting = 1000
197
200
  self.parsed = parser.parse(markup)
198
201
 
202
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
203
+ # rich pads every line with spaces to the full console width. A
204
+ # pager that reserves columns (bat's line numbers and grid) wraps
205
+ # those full-width lines, and every wrapped remainder shows up
206
+ # as an empty line. Drop the padding unless it carries a
207
+ # background colour (code blocks draw a box with it).
208
+ for line in Segment.split_lines(cast(Iterable[Segment], super().__rich_console__(console, options))):
209
+ while line and not line[-1].control:
210
+ text, style, _ = line[-1]
211
+ if style is not None and style.bgcolor is not None:
212
+ break
213
+ text = text.rstrip(" ")
214
+ if text:
215
+ line[-1] = Segment(text, style)
216
+ break
217
+ line.pop()
218
+ yield from line
219
+ yield Segment.line()
220
+
199
221
  _deep_markdown_cls = DeepMarkdown
200
222
  return DeepMarkdown
201
223
 
@@ -341,7 +363,7 @@ class Application:
341
363
  if dataclasses.is_dataclass(data)
342
364
  else data
343
365
  if isinstance(data, dict)
344
- else {data: data}
366
+ else {"data": data}
345
367
  )
346
368
 
347
369
  if text is None:
@@ -0,0 +1 @@
1
+ __version__ = "2.8.0-dev.8"
@@ -0,0 +1,21 @@
1
+ import click
2
+
3
+ _ROBOT_EXIT_CODES_ATTR = "__robotcode_robot_exit_codes__"
4
+
5
+
6
+ def robot_exit_codes(cmd: click.Command) -> click.Command:
7
+ """Mark a CLI command that passes on the exit code of a Robot Framework tool.
8
+
9
+ Such a command answers with Robot Framework's exit codes, like the `wrappable`
10
+ commands that run Robot Framework, so an error robotcode reports itself for it
11
+ exits with Robot Framework's 252 instead of Click's 1 or 2. Mark the commands
12
+ that pass on a tool's exit code without running tests - rebot, libdoc,
13
+ testdoc; `wrappable` commands are covered already.
14
+ """
15
+ setattr(cmd, _ROBOT_EXIT_CODES_ATTR, True)
16
+ return cmd
17
+
18
+
19
+ def has_robot_exit_codes(cmd: click.Command) -> bool:
20
+ """Return whether `cmd` was marked with :func:`robot_exit_codes`."""
21
+ return bool(getattr(cmd, _ROBOT_EXIT_CODES_ATTR, False))
@@ -0,0 +1,110 @@
1
+ import os
2
+ import shlex
3
+ import subprocess
4
+ import sys
5
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
6
+
7
+ import click
8
+
9
+ if TYPE_CHECKING:
10
+ from robotcode.plugin import Application
11
+
12
+ # Set in the environment once a wrapper has been applied to the process tree.
13
+ # Prevents infinite re-exec recursion and lets an outer layer (e.g. the VS Code
14
+ # debug launcher applying its own wrapper) suppress the CLI/profile re-exec.
15
+ WRAPPER_APPLIED_ENV = "ROBOTCODE_WRAPPER_APPLIED"
16
+
17
+ _WRAPPABLE_ATTR = "__robotcode_wrappable__"
18
+
19
+
20
+ def wrappable(cmd: click.Command) -> click.Command:
21
+ """Mark a CLI command as *wrappable*.
22
+
23
+ When a `wrapper` (session) command is configured, a wrappable command is
24
+ re-executed through it, because it executes Robot Framework (and may, for
25
+ example, need to run inside a specific X11/Wayland session). Commands that
26
+ do not execute Robot Framework - language server, discovery, libdoc, … -
27
+ must NOT be marked.
28
+
29
+ The re-execution happens when the command loads its configuration with
30
+ `load_robot_profile` or `handle_robot_options` of `robotcode.runner.cli.robot`,
31
+ so a marked command must do that first, before it starts a server, an
32
+ interpreter or a debugger. A marked command that loads its configuration in
33
+ another way runs without the wrapper.
34
+ """
35
+ setattr(cmd, _WRAPPABLE_ATTR, True)
36
+ return cmd
37
+
38
+
39
+ def is_wrappable(cmd: click.Command) -> bool:
40
+ """Return whether `cmd` was marked with :func:`wrappable`."""
41
+ return bool(getattr(cmd, _WRAPPABLE_ATTR, False))
42
+
43
+
44
+ def invoked_command_is_wrappable(ctx: click.Context) -> bool:
45
+ """Return whether the command that the root group invokes is marked with :func:`wrappable`.
46
+
47
+ A command that such a command runs itself, such as `robot` in `debug`, belongs to it.
48
+ """
49
+ root = ctx.find_root()
50
+ sub_name = root.invoked_subcommand
51
+ if not sub_name or not isinstance(root.command, click.Group):
52
+ return False
53
+ # Aliases resolve to the same command object, so this is alias-safe and needs
54
+ # no central name list.
55
+ sub_cmd = root.command.get_command(root, sub_name)
56
+ return sub_cmd is not None and is_wrappable(sub_cmd)
57
+
58
+
59
+ def run_under_wrapper(app: "Application", profile_wrapper: Optional[Sequence[Any]]) -> None:
60
+ """Re-execute this process through the configured ``wrapper`` command.
61
+
62
+ A wrappable command calls this once it has loaded its configuration, before it
63
+ starts anything. ``--wrapper`` overrides the ``wrapper`` of the profile
64
+ (``profile_wrapper``); ``--no-wrapper`` disables wrapping for this run. The
65
+ guard env var prevents infinite recursion and lets an outer layer suppress the
66
+ re-exec by setting it itself. Returns when no wrapper applies or for a dry run.
67
+ """
68
+ if app.config.no_wrapper or os.environ.get(WRAPPER_APPLIED_ENV):
69
+ return
70
+
71
+ # `--wrapper` overrides the profile's `wrapper`. The evaluated profile turns
72
+ # StringExpression entries into plain strings.
73
+ if app.config.wrapper:
74
+ wrapper = shlex.split(app.config.wrapper)
75
+ elif profile_wrapper:
76
+ wrapper = [str(w) for w in profile_wrapper]
77
+ else:
78
+ return
79
+
80
+ # A dry run must not start the wrapper: it would set up and tear down the
81
+ # real environment. Show it instead; the command itself then prints its own
82
+ # dry-run output.
83
+ if app.config.dry:
84
+ app.echo(f"Dry run, not executing the wrapper. Would run robotcode under: {' '.join(wrapper)}")
85
+ return
86
+
87
+ # Rebuild the invocation the way this process was actually started, so the
88
+ # re-exec finds robotcode even when it runs from a bundled path (VS Code) and
89
+ # is not installed in the interpreter. `launcher_script` is set only when we
90
+ # were started through a bundled entry (its `__main__` sets it) or an explicit
91
+ # `--launcher-script`; otherwise robotcode is importable directly and
92
+ # `-m robotcode.cli` is correct. We deliberately do NOT consult the
93
+ # ROBOTCODE_BUNDLED_ROBOTCODE_MAIN env var: VS Code sets it in every terminal,
94
+ # so a directly started robotcode would be diverted to the bundled copy.
95
+ # Mirrors the debug launcher's `debugger_script` handling.
96
+ entry = [str(app.config.launcher_script)] if app.config.launcher_script else ["-m", "robotcode.cli"]
97
+ command = [*wrapper, sys.executable, *entry, *sys.argv[1:]]
98
+ app.verbose(lambda: "Running under wrapper: " + " ".join(command))
99
+
100
+ # Run robotcode again under the wrapper; the guard env stops the spawned
101
+ # robotcode from wrapping a second time. On POSIX we replace this process with
102
+ # execvp (no extra process); Windows has no such exec, so there we spawn the
103
+ # command, wait, and forward its exit code.
104
+ os.environ[WRAPPER_APPLIED_ENV] = "1"
105
+ try:
106
+ if sys.platform == "win32":
107
+ sys.exit(subprocess.run(command).returncode)
108
+ os.execvp(command[0], command)
109
+ except OSError as e:
110
+ raise click.ClickException(f"Failed to execute wrapper command {wrapper!r}: {e}") from e
@@ -1 +0,0 @@
1
- __version__ = "2.7.0"
@@ -1,26 +0,0 @@
1
- import click
2
-
3
- # Set in the environment once a wrapper has been applied to the process tree.
4
- # Prevents infinite re-exec recursion and lets an outer layer (e.g. the VS Code
5
- # debug launcher applying its own wrapper) suppress the CLI/profile re-exec.
6
- WRAPPER_APPLIED_ENV = "ROBOTCODE_WRAPPER_APPLIED"
7
-
8
- _WRAPPABLE_ATTR = "__robotcode_wrappable__"
9
-
10
-
11
- def wrappable(cmd: click.Command) -> click.Command:
12
- """Mark a CLI command as *wrappable*.
13
-
14
- When a `wrapper` (session) command is configured, a wrappable command is
15
- re-executed through it, because it executes Robot Framework (and may, for
16
- example, need to run inside a specific X11/Wayland session). Commands that
17
- do not execute Robot Framework - language server, discovery, libdoc, … -
18
- must NOT be marked.
19
- """
20
- setattr(cmd, _WRAPPABLE_ATTR, True)
21
- return cmd
22
-
23
-
24
- def is_wrappable(cmd: click.Command) -> bool:
25
- """Return whether `cmd` was marked with :func:`wrappable`."""
26
- return bool(getattr(cmd, _WRAPPABLE_ATTR, False))