pyaccesskit 0.1.0__py3-none-any.whl
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.
- pyaccesskit/AGENT_GUIDE.md +455 -0
- pyaccesskit/__init__.py +167 -0
- pyaccesskit/__main__.py +6 -0
- pyaccesskit/_backends/__init__.py +0 -0
- pyaccesskit/_backends/access/__init__.py +1 -0
- pyaccesskit/_backends/access/design.py +415 -0
- pyaccesskit/_backends/dao/__init__.py +1 -0
- pyaccesskit/_backends/dao/profile.py +40 -0
- pyaccesskit/_backends/dao/schema.py +805 -0
- pyaccesskit/_backends/dao/typemap.py +390 -0
- pyaccesskit/_backends/fake/__init__.py +3 -0
- pyaccesskit/_backends/fake/backend.py +680 -0
- pyaccesskit/_backends/protocols.py +339 -0
- pyaccesskit/_com/__init__.py +1 -0
- pyaccesskit/_com/constants.py +394 -0
- pyaccesskit/_com/dispatch.py +50 -0
- pyaccesskit/_com/errors.py +184 -0
- pyaccesskit/_com/gateway.py +199 -0
- pyaccesskit/_com/raw.py +164 -0
- pyaccesskit/_com/runtime.py +39 -0
- pyaccesskit/_com/variants.py +72 -0
- pyaccesskit/_engines/__init__.py +48 -0
- pyaccesskit/_engines/access.py +300 -0
- pyaccesskit/_engines/inproc.py +148 -0
- pyaccesskit/_engines/probe.py +231 -0
- pyaccesskit/_ledger.py +158 -0
- pyaccesskit/_ops/__init__.py +0 -0
- pyaccesskit/_ops/design.py +127 -0
- pyaccesskit/_ops/schema.py +471 -0
- pyaccesskit/_session/__init__.py +1 -0
- pyaccesskit/_session/protocols.py +78 -0
- pyaccesskit/_session/session.py +354 -0
- pyaccesskit/_text/__init__.py +0 -0
- pyaccesskit/_text/codec.py +114 -0
- pyaccesskit/_version.py +3 -0
- pyaccesskit/_win/__init__.py +1 -0
- pyaccesskit/_win/access_process.py +348 -0
- pyaccesskit/_win/console.py +56 -0
- pyaccesskit/_win/inspector.py +53 -0
- pyaccesskit/_win/job.py +65 -0
- pyaccesskit/_win/processes.py +159 -0
- pyaccesskit/_win/watchdog.py +253 -0
- pyaccesskit/cli/__init__.py +10 -0
- pyaccesskit/cli/_output.py +101 -0
- pyaccesskit/cli/agent.py +99 -0
- pyaccesskit/cli/app.py +54 -0
- pyaccesskit/cli/cleanup.py +56 -0
- pyaccesskit/cli/doctor.py +101 -0
- pyaccesskit/cli/inspection.py +223 -0
- pyaccesskit/database.py +296 -0
- pyaccesskit/diagnostics.py +319 -0
- pyaccesskit/enums.py +258 -0
- pyaccesskit/errors.py +407 -0
- pyaccesskit/forms/__init__.py +45 -0
- pyaccesskit/forms/builder.py +295 -0
- pyaccesskit/forms/collection.py +117 -0
- pyaccesskit/forms/controls.py +157 -0
- pyaccesskit/forms/layout.py +300 -0
- pyaccesskit/forms/spec.py +169 -0
- pyaccesskit/forms/vba.py +138 -0
- pyaccesskit/maintenance.py +32 -0
- pyaccesskit/modules.py +101 -0
- pyaccesskit/objects.py +81 -0
- pyaccesskit/options.py +40 -0
- pyaccesskit/properties.py +74 -0
- pyaccesskit/py.typed +0 -0
- pyaccesskit/queries.py +190 -0
- pyaccesskit/relationships.py +143 -0
- pyaccesskit/schema/__init__.py +73 -0
- pyaccesskit/schema/_base.py +55 -0
- pyaccesskit/schema/_reserved_words.py +55 -0
- pyaccesskit/schema/columns.py +609 -0
- pyaccesskit/schema/compat.py +57 -0
- pyaccesskit/schema/expressions.py +162 -0
- pyaccesskit/schema/indexes.py +114 -0
- pyaccesskit/schema/names.py +122 -0
- pyaccesskit/schema/queries.py +192 -0
- pyaccesskit/schema/relationships.py +132 -0
- pyaccesskit/schema/tables.py +178 -0
- pyaccesskit/tables.py +333 -0
- pyaccesskit/units.py +301 -0
- pyaccesskit-0.1.0.dist-info/METADATA +201 -0
- pyaccesskit-0.1.0.dist-info/RECORD +86 -0
- pyaccesskit-0.1.0.dist-info/WHEEL +4 -0
- pyaccesskit-0.1.0.dist-info/entry_points.txt +2 -0
- pyaccesskit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Fluent builder for :class:`FormSpec` objects.
|
|
2
|
+
|
|
3
|
+
The builder never talks to Access. It accumulates controls and produces a validated :class:`FormSpec`;
|
|
4
|
+
when obtained from ``db.forms.create(...)`` it also knows how to *save* itself, which builds the whole form
|
|
5
|
+
in one atomic step (nothing is created in Access until then)::
|
|
6
|
+
|
|
7
|
+
with db.forms.create("frmCustomers", record_source="Customers") as form:
|
|
8
|
+
form.textbox("CustomerName", label="Customer name")
|
|
9
|
+
form.checkbox("IsActive")
|
|
10
|
+
form.button("cmdClose", caption="Close", on_click="DoCmd.Close acForm, Me.Name")
|
|
11
|
+
# saved here; if the block raises, nothing is created
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import warnings
|
|
17
|
+
from collections.abc import Callable, Sequence
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Generic, Literal, Self, TypeVar
|
|
20
|
+
|
|
21
|
+
from pyaccesskit.enums import RowSourceType, Section
|
|
22
|
+
from pyaccesskit.errors import PyAccessKitError
|
|
23
|
+
from pyaccesskit.forms.controls import (
|
|
24
|
+
ButtonSpec,
|
|
25
|
+
CheckBoxSpec,
|
|
26
|
+
ComboBoxSpec,
|
|
27
|
+
ControlSpec,
|
|
28
|
+
LabelSpec,
|
|
29
|
+
TextBoxSpec,
|
|
30
|
+
)
|
|
31
|
+
from pyaccesskit.forms.spec import FormSpec
|
|
32
|
+
from pyaccesskit.forms.vba import Vba
|
|
33
|
+
from pyaccesskit.schema._base import PropertyValue, build
|
|
34
|
+
from pyaccesskit.units import Length
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from typing_extensions import TypedDict, Unpack
|
|
38
|
+
|
|
39
|
+
class _ControlOptions(TypedDict, total=False):
|
|
40
|
+
name: str | None
|
|
41
|
+
section: Section
|
|
42
|
+
at: tuple[Length, Length] | None
|
|
43
|
+
width: Length | None
|
|
44
|
+
height: Length | None
|
|
45
|
+
visible: bool
|
|
46
|
+
properties: dict[str, PropertyValue]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["FormBuilder"]
|
|
50
|
+
|
|
51
|
+
_R = TypeVar("_R")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _vba(code: str | Vba | None) -> Vba | None:
|
|
55
|
+
if code is None or isinstance(code, Vba):
|
|
56
|
+
return code
|
|
57
|
+
return Vba(code)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FormBuilder(Generic[_R]):
|
|
61
|
+
"""Accumulates controls for a form and produces a :class:`FormSpec`.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
name: Form name.
|
|
65
|
+
on_save: Called with the finished spec by :meth:`save` (set by ``db.forms.create``); its return
|
|
66
|
+
value is returned by :meth:`save`.
|
|
67
|
+
**form_options: Any other :class:`FormSpec` field (``record_source``, ``caption``,
|
|
68
|
+
``default_view``...).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self, name: str, *, on_save: Callable[[FormSpec], _R] | None = None, **form_options: Any
|
|
73
|
+
) -> None:
|
|
74
|
+
self._name = name
|
|
75
|
+
self._options: dict[str, Any] = dict(form_options)
|
|
76
|
+
self._controls: list[ControlSpec] = []
|
|
77
|
+
self._on_save = on_save
|
|
78
|
+
self._state: Literal["open", "saved", "discarded"] = "open"
|
|
79
|
+
# Validate the form-level options immediately so mistakes surface at the call site.
|
|
80
|
+
build(FormSpec, f"form {name!r}", name=name, **self._options)
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------------------------ controls
|
|
83
|
+
def add(self, control: ControlSpec) -> Self:
|
|
84
|
+
"""Append an already-built control spec."""
|
|
85
|
+
self._check_open()
|
|
86
|
+
self._controls.append(control)
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def textbox(
|
|
90
|
+
self,
|
|
91
|
+
field: str | None = None,
|
|
92
|
+
*,
|
|
93
|
+
label: str | Literal[False] | None = None,
|
|
94
|
+
control_source: str | None = None,
|
|
95
|
+
format: str | None = None,
|
|
96
|
+
enabled: bool = True,
|
|
97
|
+
locked: bool = False,
|
|
98
|
+
after_update: str | Vba | None = None,
|
|
99
|
+
**options: Unpack[_ControlOptions],
|
|
100
|
+
) -> Self:
|
|
101
|
+
"""Add a text box bound to ``field`` (or showing ``control_source="=..."``)."""
|
|
102
|
+
return self.add(
|
|
103
|
+
build(
|
|
104
|
+
TextBoxSpec,
|
|
105
|
+
f"text box {options.get('name') or field!r}",
|
|
106
|
+
field=field,
|
|
107
|
+
label=label,
|
|
108
|
+
control_source=control_source,
|
|
109
|
+
format=format,
|
|
110
|
+
enabled=enabled,
|
|
111
|
+
locked=locked,
|
|
112
|
+
after_update=_vba(after_update),
|
|
113
|
+
**options,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def checkbox(
|
|
118
|
+
self,
|
|
119
|
+
field: str | None = None,
|
|
120
|
+
*,
|
|
121
|
+
label: str | Literal[False] | None = None,
|
|
122
|
+
enabled: bool = True,
|
|
123
|
+
locked: bool = False,
|
|
124
|
+
after_update: str | Vba | None = None,
|
|
125
|
+
**options: Unpack[_ControlOptions],
|
|
126
|
+
) -> Self:
|
|
127
|
+
"""Add a check box bound to ``field``."""
|
|
128
|
+
return self.add(
|
|
129
|
+
build(
|
|
130
|
+
CheckBoxSpec,
|
|
131
|
+
f"check box {options.get('name') or field!r}",
|
|
132
|
+
field=field,
|
|
133
|
+
label=label,
|
|
134
|
+
enabled=enabled,
|
|
135
|
+
locked=locked,
|
|
136
|
+
after_update=_vba(after_update),
|
|
137
|
+
**options,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def combobox(
|
|
142
|
+
self,
|
|
143
|
+
field: str | None = None,
|
|
144
|
+
*,
|
|
145
|
+
row_source: str,
|
|
146
|
+
row_source_type: RowSourceType = RowSourceType.TABLE_QUERY,
|
|
147
|
+
bound_column: int = 1,
|
|
148
|
+
column_count: int = 1,
|
|
149
|
+
column_widths: Sequence[Length] | None = None,
|
|
150
|
+
limit_to_list: bool = True,
|
|
151
|
+
label: str | Literal[False] | None = None,
|
|
152
|
+
enabled: bool = True,
|
|
153
|
+
locked: bool = False,
|
|
154
|
+
after_update: str | Vba | None = None,
|
|
155
|
+
**options: Unpack[_ControlOptions],
|
|
156
|
+
) -> Self:
|
|
157
|
+
"""Add a combo box bound to ``field`` with rows from ``row_source``."""
|
|
158
|
+
return self.add(
|
|
159
|
+
build(
|
|
160
|
+
ComboBoxSpec,
|
|
161
|
+
f"combo box {options.get('name') or field!r}",
|
|
162
|
+
field=field,
|
|
163
|
+
row_source=row_source,
|
|
164
|
+
row_source_type=row_source_type,
|
|
165
|
+
bound_column=bound_column,
|
|
166
|
+
column_count=column_count,
|
|
167
|
+
column_widths=column_widths,
|
|
168
|
+
limit_to_list=limit_to_list,
|
|
169
|
+
label=label,
|
|
170
|
+
enabled=enabled,
|
|
171
|
+
locked=locked,
|
|
172
|
+
after_update=_vba(after_update),
|
|
173
|
+
**options,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def label(self, caption: str, **options: Unpack[_ControlOptions]) -> Self:
|
|
178
|
+
"""Add a free-standing label."""
|
|
179
|
+
return self.add(build(LabelSpec, f"label {caption!r}", caption=caption, **options))
|
|
180
|
+
|
|
181
|
+
def button(
|
|
182
|
+
self,
|
|
183
|
+
name: str,
|
|
184
|
+
*,
|
|
185
|
+
caption: str,
|
|
186
|
+
on_click: str | Vba | None = None,
|
|
187
|
+
section: Section = Section.DETAIL,
|
|
188
|
+
at: tuple[Length, Length] | None = None,
|
|
189
|
+
width: Length | None = None,
|
|
190
|
+
height: Length | None = None,
|
|
191
|
+
visible: bool = True,
|
|
192
|
+
properties: dict[str, PropertyValue] | None = None,
|
|
193
|
+
) -> Self:
|
|
194
|
+
"""Add a command button; ``on_click`` is the VBA body of its Click event procedure."""
|
|
195
|
+
return self.add(
|
|
196
|
+
build(
|
|
197
|
+
ButtonSpec,
|
|
198
|
+
f"button {name!r}",
|
|
199
|
+
name=name,
|
|
200
|
+
caption=caption,
|
|
201
|
+
on_click=_vba(on_click),
|
|
202
|
+
section=section,
|
|
203
|
+
at=at,
|
|
204
|
+
width=width,
|
|
205
|
+
height=height,
|
|
206
|
+
visible=visible,
|
|
207
|
+
properties=properties or {},
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------- form-level code
|
|
212
|
+
def on_load(self, code: str | Vba) -> Self:
|
|
213
|
+
"""Set the VBA body of the form's Load event."""
|
|
214
|
+
self._check_open()
|
|
215
|
+
self._options["on_load"] = _vba(code)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def on_current(self, code: str | Vba) -> Self:
|
|
219
|
+
"""Set the VBA body of the form's Current event."""
|
|
220
|
+
self._check_open()
|
|
221
|
+
self._options["on_current"] = _vba(code)
|
|
222
|
+
return self
|
|
223
|
+
|
|
224
|
+
def module_code(self, code: str) -> Self:
|
|
225
|
+
"""Append extra VBA (helper procedures, module-level declarations) to the form's module."""
|
|
226
|
+
self._check_open()
|
|
227
|
+
existing = self._options.get("module_code") or ""
|
|
228
|
+
self._options["module_code"] = f"{existing}\n{code}" if existing else code
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------------------------ results
|
|
232
|
+
def to_spec(self) -> FormSpec:
|
|
233
|
+
"""Validate and return the finished :class:`FormSpec`."""
|
|
234
|
+
return build(
|
|
235
|
+
FormSpec,
|
|
236
|
+
f"form {self._name!r}",
|
|
237
|
+
name=self._name,
|
|
238
|
+
controls=tuple(self._controls),
|
|
239
|
+
**self._options,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def save(self) -> _R:
|
|
243
|
+
"""Build the form in Access (atomically) and return the result of ``on_save``.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
PyAccessKitError: If the builder has no save target or was already saved/discarded.
|
|
247
|
+
"""
|
|
248
|
+
self._check_open()
|
|
249
|
+
if self._on_save is None:
|
|
250
|
+
raise PyAccessKitError(
|
|
251
|
+
"this FormBuilder is standalone; use to_spec() or db.forms.create(...)"
|
|
252
|
+
)
|
|
253
|
+
spec = self.to_spec()
|
|
254
|
+
result = self._on_save(spec)
|
|
255
|
+
self._state = "saved"
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
def discard(self) -> None:
|
|
259
|
+
"""Abandon the builder without creating anything."""
|
|
260
|
+
self._state = "discarded"
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def is_saved(self) -> bool:
|
|
264
|
+
"""Whether :meth:`save` completed."""
|
|
265
|
+
return self._state == "saved"
|
|
266
|
+
|
|
267
|
+
def _check_open(self) -> None:
|
|
268
|
+
if self._state != "open":
|
|
269
|
+
raise PyAccessKitError(f"form builder {self._name!r} was already {self._state}")
|
|
270
|
+
|
|
271
|
+
# ------------------------------------------------------------------------------ context manager
|
|
272
|
+
def __enter__(self) -> Self:
|
|
273
|
+
return self
|
|
274
|
+
|
|
275
|
+
def __exit__(
|
|
276
|
+
self,
|
|
277
|
+
exc_type: type[BaseException] | None,
|
|
278
|
+
exc: BaseException | None,
|
|
279
|
+
tb: TracebackType | None,
|
|
280
|
+
) -> None:
|
|
281
|
+
if exc_type is not None:
|
|
282
|
+
self.discard()
|
|
283
|
+
elif self._state == "open":
|
|
284
|
+
self.save()
|
|
285
|
+
|
|
286
|
+
def __del__(self) -> None:
|
|
287
|
+
if getattr(self, "_state", None) == "open" and getattr(self, "_on_save", None) is not None:
|
|
288
|
+
warnings.warn(
|
|
289
|
+
f"form {self._name!r} was never saved; call .save() or use 'with db.forms.create(...)'",
|
|
290
|
+
ResourceWarning,
|
|
291
|
+
stacklevel=2,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
return f"<FormBuilder {self._name!r} controls={len(self._controls)} state={self._state}>"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Forms: the ``db.forms`` collection and :class:`Form` handles (building forms needs Microsoft Access)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from pyaccesskit._backends.protocols import ControlInfo
|
|
9
|
+
from pyaccesskit._ops import design as design_ops
|
|
10
|
+
from pyaccesskit.enums import ObjectKind
|
|
11
|
+
from pyaccesskit.errors import ObjectNotFoundError
|
|
12
|
+
from pyaccesskit.forms.builder import FormBuilder
|
|
13
|
+
from pyaccesskit.forms.layout import ResolvedForm
|
|
14
|
+
from pyaccesskit.forms.spec import FormSpec
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pyaccesskit._session.session import Session
|
|
18
|
+
|
|
19
|
+
__all__ = ["Form", "FormCollection"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Form:
|
|
23
|
+
"""A saved form (a live, name-based handle)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, session: Session, name: str) -> None:
|
|
26
|
+
self._session = session
|
|
27
|
+
self._name = name
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
"""The form name."""
|
|
32
|
+
return self._name
|
|
33
|
+
|
|
34
|
+
def controls(self) -> list[ControlInfo]:
|
|
35
|
+
"""The form's controls as saved (opens the form hidden in Design view)."""
|
|
36
|
+
return self._session.design().form_controls(self._name)
|
|
37
|
+
|
|
38
|
+
def check_opens(self) -> None:
|
|
39
|
+
"""Open the form in Form view (hidden) and close it; raises if Access reports a problem."""
|
|
40
|
+
self._session.design().check_form_opens(self._name)
|
|
41
|
+
|
|
42
|
+
def export_text(self) -> str:
|
|
43
|
+
"""The form in Access's ``SaveAsText`` format."""
|
|
44
|
+
return design_ops.export_object(self._session.design(), ObjectKind.FORM, self._name)
|
|
45
|
+
|
|
46
|
+
def rename(self, new_name: str) -> None:
|
|
47
|
+
"""Rename the form."""
|
|
48
|
+
self._session.check_writable(f"rename form {self._name!r}")
|
|
49
|
+
self._session.design().rename_object(ObjectKind.FORM, self._name, new_name)
|
|
50
|
+
self._name = new_name
|
|
51
|
+
|
|
52
|
+
def drop(self) -> None:
|
|
53
|
+
"""Delete the form."""
|
|
54
|
+
self._session.check_writable(f"drop form {self._name!r}")
|
|
55
|
+
self._session.design().delete_object(ObjectKind.FORM, self._name)
|
|
56
|
+
|
|
57
|
+
def __repr__(self) -> str:
|
|
58
|
+
return f"<Form {self._name!r}>"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FormCollection:
|
|
62
|
+
"""``db.forms``."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, session: Session) -> None:
|
|
65
|
+
self._session = session
|
|
66
|
+
|
|
67
|
+
def names(self) -> list[str]:
|
|
68
|
+
"""Form names (read through DAO; does not need Microsoft Access)."""
|
|
69
|
+
return self._session.schema().list_documents(ObjectKind.FORM)
|
|
70
|
+
|
|
71
|
+
def __iter__(self) -> Iterator[Form]:
|
|
72
|
+
return iter([Form(self._session, name) for name in self.names()])
|
|
73
|
+
|
|
74
|
+
def __len__(self) -> int:
|
|
75
|
+
return len(self.names())
|
|
76
|
+
|
|
77
|
+
def __contains__(self, name: object) -> bool:
|
|
78
|
+
return isinstance(name, str) and any(n.casefold() == name.casefold() for n in self.names())
|
|
79
|
+
|
|
80
|
+
def __getitem__(self, name: str) -> Form:
|
|
81
|
+
for actual in self.names():
|
|
82
|
+
if actual.casefold() == name.casefold():
|
|
83
|
+
return Form(self._session, actual)
|
|
84
|
+
raise ObjectNotFoundError(f"form {name!r} does not exist", kind=ObjectKind.FORM, name=name)
|
|
85
|
+
|
|
86
|
+
def create(self, name: str, *, replace: bool = False, **form_options: Any) -> FormBuilder[Form]:
|
|
87
|
+
"""Start building a form; nothing is created in Access until the builder is saved.
|
|
88
|
+
|
|
89
|
+
Example::
|
|
90
|
+
|
|
91
|
+
with db.forms.create("frmCustomers", record_source="Customers") as form:
|
|
92
|
+
form.textbox("CustomerName")
|
|
93
|
+
# built atomically here
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Form name.
|
|
97
|
+
replace: Rebuild the form if it already exists (atomically swapped in).
|
|
98
|
+
**form_options: Any :class:`~pyaccesskit.forms.FormSpec` field (``record_source``, ``caption``,
|
|
99
|
+
``default_view``...).
|
|
100
|
+
"""
|
|
101
|
+
return FormBuilder(
|
|
102
|
+
name, on_save=lambda spec: self.build(spec, replace=replace), **form_options
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def build(self, spec: FormSpec, *, replace: bool = False) -> Form:
|
|
106
|
+
"""Build a form from a finished :class:`FormSpec` (atomically)."""
|
|
107
|
+
self._session.check_writable(f"build form {spec.name!r}")
|
|
108
|
+
# design() may switch engines (closing in-process DAO), so resolve the schema backend afterwards.
|
|
109
|
+
design = self._session.design()
|
|
110
|
+
resolved: ResolvedForm = design_ops.build_form(
|
|
111
|
+
self._session.schema(), design, spec, replace=replace
|
|
112
|
+
)
|
|
113
|
+
return Form(self._session, resolved.spec.name)
|
|
114
|
+
|
|
115
|
+
def drop(self, name: str) -> None:
|
|
116
|
+
"""Delete a form."""
|
|
117
|
+
self[name].drop()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Specifications of form controls.
|
|
2
|
+
|
|
3
|
+
Each control kind is its own spec class; together they form the discriminated union :data:`ControlSpec`
|
|
4
|
+
(``kind`` is the discriminator). Positions and sizes are :class:`~pyaccesskit.units.Length` values and are
|
|
5
|
+
optional: the layout engine places controls that have no explicit ``at=``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Annotated, Literal, Self
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
from pyaccesskit.enums import ControlKind, RowSourceType, Section
|
|
15
|
+
from pyaccesskit.forms.vba import Vba
|
|
16
|
+
from pyaccesskit.schema._base import Items, PropertyValue, SpecModel
|
|
17
|
+
from pyaccesskit.schema.names import check_name
|
|
18
|
+
from pyaccesskit.units import Length
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ButtonSpec",
|
|
22
|
+
"CheckBoxSpec",
|
|
23
|
+
"ComboBoxSpec",
|
|
24
|
+
"ControlBase",
|
|
25
|
+
"ControlSpec",
|
|
26
|
+
"LabelSpec",
|
|
27
|
+
"TextBoxSpec",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ControlBase(SpecModel):
|
|
32
|
+
"""Options shared by all controls.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: Control name. Bound controls default to their field name.
|
|
36
|
+
section: Form section holding the control.
|
|
37
|
+
at: Explicit ``(left, top)`` position; ``None`` lets the layout engine place it.
|
|
38
|
+
width: Explicit width (defaults depend on the control kind).
|
|
39
|
+
height: Explicit height.
|
|
40
|
+
visible: Whether the control is visible in Form view.
|
|
41
|
+
properties: Other Access control properties to set verbatim (escape hatch).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str | None = None
|
|
45
|
+
section: Section = Section.DETAIL
|
|
46
|
+
at: tuple[Length, Length] | None = None
|
|
47
|
+
width: Length | None = None
|
|
48
|
+
height: Length | None = None
|
|
49
|
+
visible: bool = True
|
|
50
|
+
properties: dict[str, PropertyValue] = Field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
@field_validator("name")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _check_name(cls, value: str | None) -> str | None:
|
|
55
|
+
return None if value is None else check_name(value, what="control name")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def control_kind(self) -> ControlKind:
|
|
59
|
+
"""The kind of control."""
|
|
60
|
+
return ControlKind(getattr(self, "kind")) # noqa: B009 - defined by each subclass
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def bound_field(self) -> str | None:
|
|
64
|
+
"""The bound field name, if the control is bound to a field."""
|
|
65
|
+
return getattr(self, "field", None)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def attached_label(self) -> str | None:
|
|
69
|
+
"""The attached label caption, or ``None`` if the control has no attached label."""
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _BoundControl(ControlBase):
|
|
74
|
+
field: str | None = None
|
|
75
|
+
label: str | Literal[False] | None = None
|
|
76
|
+
"""Attached label: ``None`` = the field name, ``False`` = no label, or explicit text."""
|
|
77
|
+
enabled: bool = True
|
|
78
|
+
locked: bool = False
|
|
79
|
+
after_update: Vba | None = None
|
|
80
|
+
|
|
81
|
+
@field_validator("field")
|
|
82
|
+
@classmethod
|
|
83
|
+
def _check_field(cls, value: str | None) -> str | None:
|
|
84
|
+
return None if value is None else check_name(value, what="field name")
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def attached_label(self) -> str | None:
|
|
88
|
+
"""Label caption (defaults to the bound field or control name)."""
|
|
89
|
+
if self.label is False:
|
|
90
|
+
return None
|
|
91
|
+
if self.label is not None:
|
|
92
|
+
return self.label
|
|
93
|
+
return self.field or self.name
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LabelSpec(ControlBase):
|
|
97
|
+
"""A free-standing label."""
|
|
98
|
+
|
|
99
|
+
kind: Literal["label"] = "label"
|
|
100
|
+
caption: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TextBoxSpec(_BoundControl):
|
|
104
|
+
"""A text box bound to ``field`` or showing a calculated ``control_source`` (``"=[Qty]*[Price]"``)."""
|
|
105
|
+
|
|
106
|
+
kind: Literal["textbox"] = "textbox"
|
|
107
|
+
control_source: str | None = None
|
|
108
|
+
format: str | None = None
|
|
109
|
+
|
|
110
|
+
@model_validator(mode="after")
|
|
111
|
+
def _check_source(self) -> Self:
|
|
112
|
+
if self.field is not None and self.control_source is not None:
|
|
113
|
+
raise ValueError("give either field= or control_source=, not both")
|
|
114
|
+
if self.control_source is not None and not self.control_source.startswith("="):
|
|
115
|
+
raise ValueError("control_source expressions start with '=' (e.g. '=[Qty]*[Price]')")
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CheckBoxSpec(_BoundControl):
|
|
120
|
+
"""A check box (typically bound to a Yes/No field)."""
|
|
121
|
+
|
|
122
|
+
kind: Literal["checkbox"] = "checkbox"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ComboBoxSpec(_BoundControl):
|
|
126
|
+
"""A combo box whose rows come from a table/query/SQL statement or a value list."""
|
|
127
|
+
|
|
128
|
+
kind: Literal["combobox"] = "combobox"
|
|
129
|
+
row_source: str
|
|
130
|
+
row_source_type: RowSourceType = RowSourceType.TABLE_QUERY
|
|
131
|
+
bound_column: int = Field(default=1, ge=0)
|
|
132
|
+
column_count: int = Field(default=1, ge=1, le=255)
|
|
133
|
+
column_widths: Items[Length] | None = None
|
|
134
|
+
limit_to_list: bool = True
|
|
135
|
+
|
|
136
|
+
@model_validator(mode="after")
|
|
137
|
+
def _check_columns(self) -> Self:
|
|
138
|
+
if self.bound_column > self.column_count:
|
|
139
|
+
raise ValueError("bound_column cannot exceed column_count")
|
|
140
|
+
if self.column_widths is not None and len(self.column_widths) > self.column_count:
|
|
141
|
+
raise ValueError("more column_widths than column_count")
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ButtonSpec(ControlBase):
|
|
146
|
+
"""A command button, optionally running VBA when clicked."""
|
|
147
|
+
|
|
148
|
+
kind: Literal["button"] = "button"
|
|
149
|
+
caption: str
|
|
150
|
+
on_click: Vba | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
ControlSpec = Annotated[
|
|
154
|
+
LabelSpec | TextBoxSpec | CheckBoxSpec | ComboBoxSpec | ButtonSpec,
|
|
155
|
+
Field(discriminator="kind"),
|
|
156
|
+
]
|
|
157
|
+
"""Any control spec (a Pydantic discriminated union on ``kind``)."""
|