framework-py 0.1.0.dev0__py3-none-any.whl → 0.1.0.dev2__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.
framework/connections.py CHANGED
@@ -280,6 +280,13 @@ class Connection:
280
280
  def db_connect(name: str) -> Connection:
281
281
  """Open a database connection defined in the settings.
282
282
 
283
+ Args:
284
+ name: The connection's name under ``connections`` in the settings.
285
+
286
+ Returns:
287
+ A ``Connection`` that wraps the driver's connection object. Use it in a ``with`` block
288
+ to close it afterward.
289
+
283
290
  Raises:
284
291
  FrameworkError: The connection is not configured, its driver package
285
292
  is not installed, or connecting failed.
@@ -314,6 +321,18 @@ def db_query(
314
321
  Pass values in ``params``, never by formatting them into the SQL. The
315
322
  placeholder style is the driver's: ``?`` for sqlite, duckdb, and
316
323
  sqlserver, and ``%s`` for postgres and mysql.
324
+
325
+ Args:
326
+ query: The SQL query.
327
+ connection_name: The connection's name in the settings. The connection is opened for
328
+ this query and closed afterward.
329
+ params: Values for the query's placeholders, as a sequence or a mapping.
330
+
331
+ Returns:
332
+ The result rows, with the query's column names.
333
+
334
+ Raises:
335
+ FrameworkError: When the connection cannot be opened or the query fails.
317
336
  """
318
337
  with db_connect(connection_name) as connection:
319
338
  cursor = connection.cursor()
@@ -331,7 +350,21 @@ def db_query(
331
350
  def db_execute(
332
351
  query: str, connection_name: str, params: Sequence[Any] | Mapping[str, Any] | None = None
333
352
  ) -> int:
334
- """Run a statement, commit it, and return the number of rows affected."""
353
+ """Run a statement, commit it, and return the number of rows affected.
354
+
355
+ Args:
356
+ query: The SQL statement.
357
+ connection_name: The connection's name in the settings. The connection is opened for
358
+ this statement and closed afterward.
359
+ params: Values for the statement's placeholders, as a sequence or a mapping. The
360
+ placeholder style is the driver's, as in ``db_query()``.
361
+
362
+ Returns:
363
+ The driver's count of affected rows, or -1 when the driver does not report one.
364
+
365
+ Raises:
366
+ FrameworkError: When the connection cannot be opened or the statement fails.
367
+ """
335
368
  with db_connect(connection_name) as connection:
336
369
  cursor = connection.cursor()
337
370
  try:
@@ -355,6 +388,16 @@ def db_transaction(connection: Connection) -> Iterator[Connection]:
355
388
 
356
389
  with fw.db_connect("warehouse") as conn, fw.db_transaction(conn):
357
390
  conn.cursor().execute("UPDATE ...")
391
+
392
+ Args:
393
+ connection: An open connection from ``db_connect()``.
394
+
395
+ Returns:
396
+ A context manager that yields ``connection``.
397
+
398
+ Raises:
399
+ FrameworkError: When the block raises an exception. The transaction is rolled back and
400
+ the original exception is chained as the cause.
358
401
  """
359
402
  if connection._in_transaction:
360
403
  yield connection
framework/console.py CHANGED
@@ -10,7 +10,12 @@ from dataclasses import dataclass, field
10
10
 
11
11
  @dataclass
12
12
  class CapturedOutput:
13
- """Text printed inside a ``capture_output()`` block, stdout and stderr together."""
13
+ """Text printed inside a ``capture_output()`` block, stdout and stderr together.
14
+
15
+ Attributes:
16
+ output: Everything printed to stdout and stderr in the block, in order. It is empty
17
+ until the block ends.
18
+ """
14
19
 
15
20
  output: str = ""
16
21
  _buffer: io.StringIO = field(default_factory=io.StringIO, repr=False)
framework/create.py CHANGED
@@ -23,15 +23,15 @@ from typing import Any
23
23
  from framework import _yaml, db
24
24
  from framework._log import inform, show_messages
25
25
  from framework.ai import write_ai_files
26
- from framework.config_home import global_settings, project_register
26
+ from framework.config_home import config_dir, global_settings, project_register
27
27
  from framework.errors import FrameworkError
28
28
  from framework.quarto import quarto_generate_all
29
- from framework.spec.check import spec_check
29
+ from framework.spec.check import ASSISTANTS, spec_check
30
30
  from framework.spec.merge import SpecError
31
31
  from framework.spec.resolve import TYPES, author_normalize, spec_resolve
32
32
 
33
33
  CHECKOUT_ALLOWLIST = frozenset({".git", ".github", ".framework", "README.md", "LICENSE"})
34
- _FILE_LIKE = re.compile(r"\.(qmd|Rmd|R|md|py|ipynb)$")
34
+ FILE_LIKE = re.compile(r"\.(qmd|Rmd|R|md|py|ipynb)$")
35
35
  _TEMPLATE_VARS = ("project_name", "author", "author_email", "affiliation", "date")
36
36
  _SPLIT_TYPES = ("project", "project_sensitive")
37
37
  # Lines that ignore .env (or re-include it with a leading "!"), as in R.
@@ -95,13 +95,13 @@ def _assert_checkout(root: Path) -> None:
95
95
 
96
96
  def _create_directories(root: Path, spec: Mapping[str, Any]) -> None:
97
97
  for path in (spec.get("directories") or {}).values():
98
- if path and not path.startswith("outputs/") and not _FILE_LIKE.search(path):
98
+ if path and not path.startswith("outputs/") and not FILE_LIKE.search(path):
99
99
  (root / path).mkdir(parents=True, exist_ok=True)
100
100
  for entry in (spec.get("extra_directories") or {}).values():
101
101
  if entry.get("path"):
102
102
  (root / entry["path"]).mkdir(parents=True, exist_ok=True)
103
103
  for key, path in (spec.get("render_dirs") or {}).items():
104
- if path and "notebook" in key.lower() and not _FILE_LIKE.search(path):
104
+ if path and "notebook" in key.lower() and not FILE_LIKE.search(path):
105
105
  (root / path).mkdir(parents=True, exist_ok=True)
106
106
 
107
107
 
@@ -401,6 +401,24 @@ def project_create_from_layers(
401
401
 
402
402
  With ``interactive=False`` (the default) it runs headless for automation,
403
403
  as ``project_create(headless=True)`` describes.
404
+
405
+ Args:
406
+ layers_file: A JSON file with ``spec_version`` (which must be 1), ``name``,
407
+ ``layers``, and optionally ``author`` and ``source``. The layers are resolved with
408
+ ``spec_resolve()``.
409
+ path: The project folder. Default: the current directory. In headless mode it must be
410
+ a fresh checkout. Otherwise it must not exist.
411
+ interactive: Create the project in regular mode, with git initialization and a
412
+ registry entry. Nothing is prompted for either way. Default: False, which creates
413
+ it headless.
414
+
415
+ Returns:
416
+ The project folder.
417
+
418
+ Raises:
419
+ FrameworkError: When the layers file cannot be read, has an unsupported
420
+ ``spec_version``, or has no name, or when the project cannot be created there.
421
+ SpecError: When the resolved spec is invalid.
404
422
  """
405
423
  source = Path(layers_file)
406
424
  try:
@@ -421,14 +439,12 @@ def project_create_from_layers(
421
439
  return project_create(spec, path, headless=not interactive)
422
440
 
423
441
 
424
- def user_layer(type: str) -> dict[str, Any]:
425
- """The user's defaults from the global settings, as a spec layer.
442
+ def _defaults_layer(settings: Mapping[str, Any], type: str) -> dict[str, Any]:
443
+ """A spec layer from settings in the global-settings format.
426
444
 
427
- Reads ``author`` and the ``defaults`` section (scaffold, AI assistants,
428
- git, and the directories for this project type) of ``settings.yml`` in
429
- ``config_dir()``.
445
+ Reads ``author``, the ``defaults`` section (scaffold, AI assistants, and
446
+ git), and the directories of this project type.
430
447
  """
431
- settings = global_settings()
432
448
  raw_defaults = settings.get("defaults")
433
449
  defaults: dict[str, Any] = raw_defaults if isinstance(raw_defaults, dict) else {}
434
450
  layer: dict[str, Any] = {}
@@ -446,8 +462,13 @@ def user_layer(type: str) -> dict[str, Any]:
446
462
  layer["ai"] = {
447
463
  key: value for key, value in (("enabled", ai.get("enabled")),) if value is not None
448
464
  }
449
- if isinstance(assistants, list):
450
- layer["ai"]["assistants"] = {name: True for name in assistants}
465
+ if isinstance(assistants, list) and assistants:
466
+ # The list replaces the selection, as in R: assistants it leaves
467
+ # out (Claude Code included) are turned off.
468
+ selected = {name: True for name in assistants}
469
+ layer["ai"]["assistants"] = {name: False for name in ASSISTANTS} | selected
470
+ elif isinstance(assistants, dict):
471
+ layer["ai"]["assistants"] = assistants
451
472
  git = defaults.get("git")
452
473
  if isinstance(git, dict):
453
474
  layer["git"] = {
@@ -462,6 +483,29 @@ def user_layer(type: str) -> dict[str, Any]:
462
483
  return layer
463
484
 
464
485
 
486
+ def user_layer(type: str) -> dict[str, Any]:
487
+ """The user's defaults from the global settings, as a spec layer.
488
+
489
+ Reads ``author`` and the ``defaults`` section (scaffold, AI assistants,
490
+ git, and the directories for this project type) of ``settings.yml`` in
491
+ ``config_dir()``. A list in ``defaults.ai.assistants`` replaces the
492
+ default selection.
493
+ """
494
+ return _defaults_layer(global_settings(), type)
495
+
496
+
497
+ def catalog_layer(type: str) -> dict[str, Any]:
498
+ """The user's settings catalog, ``settings-catalog.yml`` in ``config_dir()``, as a spec layer.
499
+
500
+ R's ``new()`` starts from the settings catalog with this file merged over
501
+ it, so Python applies it too, below the global settings. Its keys are read
502
+ as in the global settings. Without the file the layer is empty.
503
+ """
504
+ path = config_dir() / "settings-catalog.yml"
505
+ catalog = _yaml.read(path) if path.is_file() else None
506
+ return _defaults_layer(catalog, type) if isinstance(catalog, dict) else {}
507
+
508
+
465
509
  def new(
466
510
  name: str | None = None,
467
511
  location: str | Path | None = None,
@@ -510,6 +554,7 @@ def new(
510
554
  raise FrameworkError(f"Invalid project type. Must be one of: {', '.join(TYPES)}")
511
555
 
512
556
  layers: Sequence[Any] = [
557
+ {"kind": "user", "key": "settings-catalog", "layer": catalog_layer(type)},
513
558
  {"kind": "user", "layer": user_layer(type)},
514
559
  {"kind": "project", "layer": {"type": type, "language": "python"}},
515
560
  ]
@@ -559,7 +604,7 @@ def project_add_directory(project_path: str | Path, key: str, label: str, path:
559
604
  """
560
605
  from framework.spec.check import is_safe_path
561
606
 
562
- root = Path(project_path)
607
+ root = Path(project_path).expanduser()
563
608
  if not is_safe_path(path):
564
609
  raise FrameworkError(f"Path must be relative and stay inside the project: {path}")
565
610
  settings_file = next(
framework/data.py CHANGED
@@ -22,7 +22,7 @@ import pandas as pd
22
22
  from framework import _yaml, db
23
23
  from framework._log import inform
24
24
  from framework._paths import in_root, relative_to_root
25
- from framework.catalog import DataInfo, data_info
25
+ from framework.catalog import DataInfo, catalog_locate, data_info
26
26
  from framework.errors import (
27
27
  DataChangedWarning,
28
28
  DataNotFoundError,
@@ -57,7 +57,9 @@ _DIRECT_TYPES = {
57
57
  "parquet": "parquet",
58
58
  }
59
59
 
60
- # Extension -> type recorded by data_add(), as R's data_add() does.
60
+ # Extension -> type recorded by data_add(). R's data_add() records .por as
61
+ # spss and .xpt as sas, which its own data_read() then opens with the wrong
62
+ # reader. Python records the reader types, which R reads too.
61
63
  _ADD_TYPES = {
62
64
  "csv": "csv",
63
65
  "tsv": "tsv",
@@ -69,12 +71,16 @@ _ADD_TYPES = {
69
71
  "dta": "stata",
70
72
  "sav": "spss",
71
73
  "zsav": "spss",
72
- "por": "spss",
74
+ "por": "spss_por",
73
75
  "sas7bdat": "sas",
74
- "xpt": "sas",
76
+ "xpt": "sas_xpt",
75
77
  "parquet": "parquet",
76
78
  }
77
79
 
80
+ # Catalog entries R's data_add() wrote for these extensions carry the generic
81
+ # type, so the extension picks the reader.
82
+ _GENERIC_TYPES = {("spss", ".por"): "spss_por", ("sas", ".xpt"): "sas_xpt"}
83
+
78
84
  _SAVE_TYPES = ("csv", "parquet")
79
85
 
80
86
  _R_ONLY = (
@@ -236,7 +242,7 @@ def data_read(
236
242
  format Python cannot read.
237
243
  IntegrityError: A locked file changed since its hash was recorded.
238
244
  """
239
- direct = Path(path)
245
+ direct = Path(path).expanduser()
240
246
  if direct.is_file():
241
247
  extension = direct.suffix.lower().lstrip(".")
242
248
  if extension not in _DIRECT_TYPES:
@@ -262,8 +268,9 @@ def data_read(
262
268
  FrameworkWarning,
263
269
  stacklevel=2,
264
270
  )
271
+ file_type = _GENERIC_TYPES.get((info.type, info.path.suffix.lower()), info.type)
265
272
  try:
266
- data: pd.DataFrame = _reader(info.type)(info.path, delimiter, keep_attributes, dict(kwargs))
273
+ data: pd.DataFrame = _reader(file_type)(info.path, delimiter, keep_attributes, dict(kwargs))
267
274
  except (FrameworkError, ImportError):
268
275
  raise
269
276
  except Exception as error:
@@ -306,10 +313,17 @@ def _save_target(path: str, type: str | None) -> tuple[Path, str, bool]:
306
313
  )
307
314
 
308
315
  config = settings_read()
309
- directories = config.get("directories") or {}
310
- key = f"{parts[0]}_{parts[1]}"
311
- folder = directories.get(key) if isinstance(directories, dict) else None
312
- if not isinstance(folder, str) or not folder:
316
+ directories = config.get("directories")
317
+ if not isinstance(directories, dict):
318
+ directories = {}
319
+ # The longest run of leading parts that names a directory key selects the
320
+ # folder, so inputs.private.raw.x uses inputs_private_raw. R joins only
321
+ # the first two parts, so it cannot reach three-part keys.
322
+ for end in range(len(parts) - 1, 1, -1):
323
+ folder = directories.get("_".join(parts[:end]))
324
+ if isinstance(folder, str) and folder:
325
+ break
326
+ else:
313
327
  available = ", ".join(k.replace("_", ".") for k in directories) or "none"
314
328
  raise FrameworkError(
315
329
  f"Unknown directory '{parts[0]}.{parts[1]}'. Use a configured directory.\n\n"
@@ -318,7 +332,7 @@ def _save_target(path: str, type: str | None) -> tuple[Path, str, bool]:
318
332
  " data_save(df, 'outputs.private.results')"
319
333
  )
320
334
  type = type or "parquet"
321
- target = in_root(folder, project_root()) / f"{'_'.join(parts[2:])}.{type}"
335
+ target = in_root(folder, project_root()) / f"{'_'.join(parts[end:])}.{type}"
322
336
  return target, type, True
323
337
 
324
338
 
@@ -335,10 +349,11 @@ def data_save(
335
349
  Args:
336
350
  data: The data to save.
337
351
  path: A catalog name such as ``"inputs.intermediate.cleaned"`` or a
338
- file path ending in ``.csv`` or ``.parquet``. A name's first two
339
- parts select a folder from ``directories`` (``inputs_intermediate``),
340
- and the rest becomes the file name. A name is also added to the
341
- data catalog, so ``data_read()`` finds it.
352
+ file path ending in ``.csv`` or ``.parquet``. A name's leading
353
+ parts select a folder from ``directories``: the longest run that
354
+ names a key, such as ``inputs_intermediate`` or
355
+ ``inputs_private_raw``. The rest becomes the file name. A name is
356
+ also added to the data catalog, so ``data_read()`` finds it.
342
357
  type: ``"parquet"`` or ``"csv"``. Default: from the file extension,
343
358
  or Parquet for a catalog name.
344
359
  delimiter: Delimiter for CSV files: comma, tab, semicolon, or space.
@@ -440,13 +455,18 @@ def _catalog_set(name: str, entry: dict[str, Any]) -> None:
440
455
  if not isinstance(current, dict):
441
456
  current = {}
442
457
 
443
- node = current
444
- parts = name.split(".")
445
- for part in parts[:-1]:
446
- if not isinstance(node.get(part), dict):
447
- node[part] = {}
448
- node = node[part]
449
- node[parts[-1]] = entry
458
+ # An entry already written with dotted keys is updated where it is.
459
+ existing = catalog_locate(name, current)
460
+ if existing is not None:
461
+ existing[0][existing[1]] = entry
462
+ else:
463
+ node = current
464
+ parts = name.split(".")
465
+ for part in parts[:-1]:
466
+ if not isinstance(node.get(part), dict):
467
+ node[part] = {}
468
+ node = node[part]
469
+ node[parts[-1]] = entry
450
470
 
451
471
  if split_path is not None:
452
472
  _yaml.write(split_path, {"data": current})
@@ -478,7 +498,7 @@ def data_add(
478
498
  The catalog entry. Its path is relative to the project root, so the
479
499
  project still works after it is moved or cloned.
480
500
  """
481
- path = Path(file_path)
501
+ path = Path(file_path).expanduser()
482
502
  if not path.is_file():
483
503
  raise DataNotFoundError(f"File not found: {file_path}")
484
504
  path = path.resolve()
@@ -143,12 +143,30 @@ _GITIGNORE = {
143
143
 
144
144
 
145
145
  def load_settings_catalog(include_user: bool = True, validate: bool = True) -> dict[str, Any]:
146
- """The defaults new Python projects start from.
146
+ """Framework's package defaults, with the user's settings catalog merged over them.
147
147
 
148
148
  The catalog has ``defaults`` (Framework's package defaults, layer 0 of
149
149
  ``spec_resolve()``) and ``project_types``, each with its ``.gitignore``
150
150
  text. With ``include_user``, ``settings-catalog.yml`` in ``config_dir()``
151
151
  is merged over it.
152
+
153
+ ``new()`` starts from the same package defaults, applies
154
+ ``settings-catalog.yml`` over them as R's ``new()`` does, and then
155
+ applies the global ``settings.yml``. It reads ``author``, ``defaults.scaffold``,
156
+ ``defaults.ai``, ``defaults.git``, and ``project_types.<type>.directories``
157
+ from the file, with the same keys as the global settings.
158
+
159
+ Args:
160
+ include_user: Merge ``settings-catalog.yml`` from ``config_dir()`` over the shipped
161
+ catalog when that file exists.
162
+ validate: Check that the result has ``defaults`` and ``project_types`` mappings.
163
+
164
+ Returns:
165
+ The catalog, with ``meta``, ``defaults``, and ``project_types`` keys.
166
+
167
+ Raises:
168
+ SettingsError: When ``validate`` is true and ``defaults`` or ``project_types`` is
169
+ missing or not a mapping.
152
170
  """
153
171
  shipped = files("framework").joinpath("templates")
154
172
  defaults = _yaml.load(shipped.joinpath("defaults.yml").read_text(encoding="utf-8")) or {}
@@ -208,12 +226,32 @@ def _template_file(name: str) -> str:
208
226
 
209
227
 
210
228
  def framework_template_path(name: str) -> Path:
211
- """The file your version of a template is saved in (it may not exist yet)."""
229
+ """The file your version of a template is saved in (it may not exist yet).
230
+
231
+ Args:
232
+ name: The template. One of "notebook", "script", or "presentation".
233
+
234
+ Returns:
235
+ The path in ``templates_dir()`` for that template.
236
+
237
+ Raises:
238
+ FrameworkError: When ``name`` is not a known template.
239
+ """
212
240
  return templates_dir() / _template_file(name)
213
241
 
214
242
 
215
243
  def read_framework_template(name: str) -> str:
216
- """A template's text: yours when you saved one, otherwise Framework's."""
244
+ """A template's text: yours when you saved one, otherwise Framework's.
245
+
246
+ Args:
247
+ name: The template. One of "notebook", "script", or "presentation".
248
+
249
+ Returns:
250
+ The template's text.
251
+
252
+ Raises:
253
+ FrameworkError: When ``name`` is not a known template.
254
+ """
217
255
  path = framework_template_path(name)
218
256
  if path.is_file():
219
257
  return path.read_text(encoding="utf-8")
@@ -222,7 +260,19 @@ def read_framework_template(name: str) -> str:
222
260
 
223
261
 
224
262
  def write_framework_template(name: str, contents: str) -> Path:
225
- """Save your version of a template. Returns its path."""
263
+ """Save your version of a template. Returns its path.
264
+
265
+ Args:
266
+ name: The template. One of "notebook", "script", or "presentation".
267
+ contents: The template's text. A trailing newline is added when it has none.
268
+
269
+ Returns:
270
+ The path the template was written to.
271
+
272
+ Raises:
273
+ FrameworkError: When ``name`` is not a known template.
274
+ TypeError: When ``contents`` is not a string.
275
+ """
226
276
  if not isinstance(contents, str):
227
277
  raise TypeError("contents must be text")
228
278
  path = framework_template_path(name)
@@ -232,7 +282,17 @@ def write_framework_template(name: str, contents: str) -> Path:
232
282
 
233
283
 
234
284
  def reset_framework_template(name: str) -> Path:
235
- """Remove your version of a template, so Framework's is used again."""
285
+ """Remove your version of a template, so Framework's is used again.
286
+
287
+ Args:
288
+ name: The template. One of "notebook", "script", or "presentation".
289
+
290
+ Returns:
291
+ The path of the removed file. Nothing fails when it did not exist.
292
+
293
+ Raises:
294
+ FrameworkError: When ``name`` is not a known template.
295
+ """
236
296
  path = framework_template_path(name)
237
297
  path.unlink(missing_ok=True)
238
298
  return path
framework/hooks.py CHANGED
@@ -59,7 +59,7 @@ def hook_script() -> str:
59
59
 
60
60
 
61
61
  def _settings_path(settings_file: str | Path | None) -> Path:
62
- path = Path(settings_file) if settings_file is not None else find_settings_file()
62
+ path = Path(settings_file).expanduser() if settings_file is not None else find_settings_file()
63
63
  if path is None or not path.is_file():
64
64
  raise FrameworkError("Settings file not found (expected settings.yml or config.yml)")
65
65
  return path.resolve()
@@ -150,8 +150,18 @@ def git_hooks_install(
150
150
  A pre-commit hook Framework wrote earlier (by R or Python) is replaced. Any
151
151
  other pre-commit hook is kept unless ``force`` is True.
152
152
 
153
+ Args:
154
+ settings_file: The project's settings file. Default: the settings.yml or config.yml
155
+ found from the current directory.
156
+ force: Replace a pre-commit hook that Framework did not write.
157
+ verbose: Print what was done.
158
+
153
159
  Returns:
154
- Whether the hook was installed.
160
+ Whether the hook was installed. False outside a git repository, when no check is
161
+ enabled, or when another hook is kept.
162
+
163
+ Raises:
164
+ FrameworkError: When the settings file is not found.
155
165
  """
156
166
  path = _settings_path(settings_file)
157
167
  root = path.parent
@@ -181,6 +191,12 @@ def git_hooks_install(
181
191
  def git_hooks_uninstall(settings_file: str | Path | None = None, *, verbose: bool = True) -> bool:
182
192
  """Remove Framework's pre-commit hook. A hook Framework did not write is kept.
183
193
 
194
+ Args:
195
+ settings_file: The project's settings file, whose folder holds the repository.
196
+ Default: the folder of the settings file found from the current directory, or the
197
+ current directory when there is none.
198
+ verbose: Print what was done.
199
+
184
200
  Returns:
185
201
  Whether a hook was removed.
186
202
  """
@@ -207,8 +223,20 @@ def git_hooks_enable(
207
223
  ) -> bool:
208
224
  """Turn on one hook check in the settings and (re)install the hook.
209
225
 
226
+ Args:
227
+ hook_name: The check. One of "data_security" or "check_sensitive_dirs". The older
228
+ name "warn_unignored_sensitive" is accepted for "check_sensitive_dirs".
229
+ settings_file: The project's settings file. Default: the settings.yml or config.yml
230
+ found from the current directory.
231
+ verbose: Print what was done.
232
+
210
233
  Returns:
211
234
  Whether the hook is installed.
235
+
236
+ Raises:
237
+ ValueError: When ``hook_name`` is not a known check, including the removed
238
+ "ai_sync".
239
+ FrameworkError: When the settings file is not found.
212
240
  """
213
241
  name = _hook_name(hook_name)
214
242
  path = _settings_path(settings_file)
@@ -225,8 +253,21 @@ def git_hooks_disable(
225
253
  ``"ai_sync"`` is accepted so older settings can be cleaned up: it is set
226
254
  to false.
227
255
 
256
+ Args:
257
+ hook_name: The check. One of "data_security", "check_sensitive_dirs", or
258
+ "ai_sync". The older name "warn_unignored_sensitive" is accepted for
259
+ "check_sensitive_dirs", and turning that check off also turns off the older
260
+ setting.
261
+ settings_file: The project's settings file. Default: the settings.yml or config.yml
262
+ found from the current directory.
263
+ verbose: Print what was done.
264
+
228
265
  Returns:
229
266
  Whether the hook is still installed.
267
+
268
+ Raises:
269
+ ValueError: When ``hook_name`` is not a known check.
270
+ FrameworkError: When the settings file is not found.
230
271
  """
231
272
  name = "ai_sync" if hook_name == "ai_sync" else _hook_name(hook_name)
232
273
  path = _settings_path(settings_file)
@@ -247,6 +288,10 @@ def git_hooks_disable(
247
288
  def git_hooks_list(settings_file: str | Path | None = None) -> pd.DataFrame:
248
289
  """The hook checks and whether each is enabled.
249
290
 
291
+ Args:
292
+ settings_file: The project's settings file. Default: the settings.yml or config.yml
293
+ found from the current directory.
294
+
250
295
  Returns:
251
296
  A DataFrame with columns hook, enabled, and description. Its
252
297
  ``attrs["installed"]`` says whether the pre-commit hook is installed.