revitdevtool-pytest 0.0.2__tar.gz → 0.0.3__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 (20) hide show
  1. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/CHANGELOG.md +12 -0
  2. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/PKG-INFO +1 -1
  3. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/pyproject.toml +1 -1
  4. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/dialog_resolver.py +37 -10
  5. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/discovery.py +13 -0
  6. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/plugin.py +11 -8
  7. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/.github/workflows/publish.yml +0 -0
  8. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/.gitignore +0 -0
  9. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/.vscode/settings.json +0 -0
  10. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/LICENSE +0 -0
  11. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/README.md +0 -0
  12. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/__init__.py +0 -0
  13. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/bridge.py +0 -0
  14. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/constants.py +0 -0
  15. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/models.py +0 -0
  16. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/src/revitdevtool_pytest/serializer.py +0 -0
  17. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/tests/conftest.py +0 -0
  18. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/tests/test_revit_basic.py +0 -0
  19. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/tests/test_revit_transactions.py +0 -0
  20. {revitdevtool_pytest-0.0.2 → revitdevtool_pytest-0.0.3}/tests/test_smoke.py +0 -0
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.0.3] - 2026-04-10
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Revit auto-launch dialog handling by starting the startup dialog resolver immediately after spawning Revit, before waiting for the named pipe.
10
+ - Fixed startup dialog button selection to prefer safe allow-list actions such as `Always Load` and `Load Once`.
11
+ - Prevented accidental clicks on destructive dialog actions such as `Do Not Load`, `Cancel`, and `No`.
12
+
13
+ ### Notes
14
+
15
+ - This release focuses on reliable auto-launch behavior when Revit shows unsigned add-in security dialogs during startup.
16
+
5
17
  ## [0.0.2] - 2026-04-10
6
18
 
7
19
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: revitdevtool_pytest
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge
5
5
  Project-URL: Homepage, https://github.com/trgiangv/RevitDevTool.PyTest
6
6
  Project-URL: Repository, https://github.com/trgiangv/RevitDevTool.PyTest
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "revitdevtool_pytest"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -59,7 +59,10 @@ class DialogResolverOptions:
59
59
  "Autodesk", "Revit", "Load", "Security", "Warning", "Add-in", "Addin",
60
60
  ])
61
61
  preferred_button_keywords: list[str] = field(default_factory=lambda: [
62
- "Always Load", "Load", "OK", "Yes", "Accept", "Continue", "Close",
62
+ "Always Load", "Load Once", "Load", "OK", "Yes", "Accept", "Continue", "Close",
63
+ ])
64
+ blocked_button_keywords: list[str] = field(default_factory=lambda: [
65
+ "Do Not Load", "Cancel", "No",
63
66
  ])
64
67
 
65
68
 
@@ -106,17 +109,25 @@ class StartupDialogResolver:
106
109
  return any(kw.lower() in lower for kw in self._opts.dialog_title_keywords)
107
110
 
108
111
  def _find_button(self, parent: int) -> int | None:
109
- result: list[int] = []
112
+ result: list[tuple[int, int]] = []
110
113
 
111
114
  @_EnumWindowsProc
112
115
  def _cb(child: int, _: int) -> bool:
113
- if _is_preferred_button(child, self._opts.preferred_button_keywords):
114
- result.append(child)
115
- return False
116
+ score = _get_button_score(
117
+ child,
118
+ self._opts.preferred_button_keywords,
119
+ self._opts.blocked_button_keywords,
120
+ )
121
+ if score is not None:
122
+ result.append((score, child))
116
123
  return True
117
124
 
118
125
  _user32.EnumChildWindows(parent, _cb, 0)
119
- return result[0] if result else None
126
+ if not result:
127
+ return None
128
+
129
+ result.sort(key=lambda item: item[0])
130
+ return result[0][1]
120
131
 
121
132
  def _enum_dialog_windows(self) -> list[int]:
122
133
  windows: list[int] = []
@@ -131,14 +142,30 @@ class StartupDialogResolver:
131
142
  return windows
132
143
 
133
144
 
134
- def _is_preferred_button(hwnd: int, keywords: list[str]) -> bool:
145
+ def _get_button_score(hwnd: int, keywords: list[str], blocked_keywords: list[str]) -> int | None:
135
146
  if _get_class_name(hwnd).lower() != _BUTTON_CLASS:
136
- return False
147
+ return None
148
+
137
149
  text = _get_window_text(hwnd)
138
150
  if not text:
139
- return False
151
+ return None
152
+
140
153
  text_lower = text.lower()
141
- return any(kw.lower() in text_lower for kw in keywords)
154
+
155
+ if any(kw.lower() in text_lower for kw in blocked_keywords):
156
+ return None
157
+
158
+ for index, keyword in enumerate(keywords):
159
+ keyword_lower = keyword.lower()
160
+ if text_lower == keyword_lower:
161
+ return index
162
+
163
+ for index, keyword in enumerate(keywords):
164
+ keyword_lower = keyword.lower()
165
+ if keyword_lower in text_lower:
166
+ return index + len(keywords)
167
+
168
+ return None
142
169
 
143
170
 
144
171
  def _is_target_dialog(hwnd: int, target_pid: int) -> bool:
@@ -87,6 +87,19 @@ def launch_revit(
87
87
  return wait_for_revit_pipe(version, timeout_s=wait_timeout_s)
88
88
 
89
89
 
90
+ def start_revit(version: int) -> int:
91
+ """Start Revit with ``/nosplash`` and return the spawned process id."""
92
+ exe_path = find_revit_path(version)
93
+ if exe_path is None:
94
+ raise FileNotFoundError(f"Revit {version} installation not found.")
95
+
96
+ process = subprocess.Popen( # noqa: S603
97
+ [exe_path, REVIT_NOSPLASH],
98
+ creationflags=subprocess.DETACHED_PROCESS,
99
+ )
100
+ return int(process.pid)
101
+
102
+
90
103
  def wait_for_revit_pipe(
91
104
  version: int | None = None,
92
105
  timeout_s: float = DEFAULT_LAUNCH_TIMEOUT_S,
@@ -22,7 +22,7 @@ from .constants import (
22
22
  OPT_VERSION,
23
23
  PLUGIN_NAME,
24
24
  )
25
- from .discovery import find_revit_path, find_revit_pipes, launch_revit, select_instance
25
+ from .discovery import find_revit_path, find_revit_pipes, select_instance, start_revit, wait_for_revit_pipe
26
26
  from .models import TestOutcome
27
27
  from .serializer import serialize_test
28
28
 
@@ -152,22 +152,25 @@ def _auto_launch(version: int, config: pytest.Config) -> RevitInstance:
152
152
  )
153
153
 
154
154
  print(f"{PLUGIN_NAME}: Launching Revit {version}...")
155
- instance = launch_revit(version, wait_timeout_s=timeout)
156
155
 
157
- if instance is None:
158
- pytest.exit(
159
- f"{PLUGIN_NAME}: Revit {version} launched but Named Pipe did not appear within {timeout}s.",
160
- returncode=EXIT_CODE_CONFIG_ERROR,
161
- )
156
+ process_id = start_revit(version)
162
157
 
163
158
  try:
164
159
  from .dialog_resolver import StartupDialogResolver as _Resolver
165
160
 
166
- _dialog_resolver = _Resolver(instance.process_id)
161
+ _dialog_resolver = _Resolver(process_id)
167
162
  _dialog_resolver.start()
168
163
  except ImportError:
169
164
  pass
170
165
 
166
+ instance = wait_for_revit_pipe(version, timeout_s=timeout)
167
+
168
+ if instance is None:
169
+ pytest.exit(
170
+ f"{PLUGIN_NAME}: Revit {version} launched but Named Pipe did not appear within {timeout}s.",
171
+ returncode=EXIT_CODE_CONFIG_ERROR,
172
+ )
173
+
171
174
  print(f"{PLUGIN_NAME}: Connected to Revit {instance.version} (pid={instance.process_id})")
172
175
  return instance
173
176