griffecli 2.0.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.
griffecli/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ # This top-level module imports all public names from the CLI package,
2
+ # and exposes them as public objects.
3
+
4
+ """Griffe CLI package.
5
+
6
+ The CLI (Command Line Interface) for the griffe library.
7
+ This package provides command-line tools for interacting with griffe.
8
+
9
+ ## CLI entrypoints
10
+
11
+ - [`griffecli.main`][]: Run the main program.
12
+ - [`griffecli.check`][]: Check for API breaking changes in two versions of the same package.
13
+ - [`griffecli.dump`][]: Load packages data and dump it as JSON.
14
+ - [`griffecli.get_parser`][]: Get the argument parser for the CLI.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from griffecli._internal.cli import DEFAULT_LOG_LEVEL, check, dump, get_parser, main
20
+
21
+ __all__ = [
22
+ "DEFAULT_LOG_LEVEL",
23
+ "check",
24
+ "dump",
25
+ "get_parser",
26
+ "main",
27
+ ]
griffecli/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ # Entry-point module, in case you use `python -m griffecli`.
2
+ #
3
+ # Why does this file exist, and why `__main__`? For more info, read:
4
+ #
5
+ # - https://www.python.org/dev/peps/pep-0338/
6
+ # - https://docs.python.org/3/using/cmdline.html#cmdoption-m
7
+
8
+ import sys
9
+
10
+ from griffecli._internal.cli import main
11
+
12
+ if __name__ == "__main__":
13
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1 @@
1
+ # Internal modules for the griffecli package.
@@ -0,0 +1,627 @@
1
+ # This module contains all CLI-related things.
2
+ # Why does this file exist, and why not put this in `__main__`?
3
+ #
4
+ # We might be tempted to import things from `__main__` later,
5
+ # but that will cause problems; the code will get executed twice:
6
+ #
7
+ # - When we run `python -m griffecli`, Python will execute
8
+ # `__main__.py` as a script. That means there won't be any
9
+ # `griffecli.__main__` in `sys.modules`.
10
+ # - When you import `__main__` it will get executed again (as a module) because
11
+ # there's no `griffecli.__main__` in `sys.modules`.
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import logging
18
+ import os
19
+ import re
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import IO, TYPE_CHECKING, Any, Callable
24
+
25
+ import colorama
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Sequence
29
+
30
+ from griffe._internal.docstrings.parsers import DocstringOptions, DocstringStyle
31
+ from griffe._internal.enumerations import ExplanationStyle, Parser
32
+ from griffe._internal.extensions.base import Extension, Extensions
33
+ from griffe._internal.loader import GriffeLoader
34
+
35
+
36
+ DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper()
37
+ """The default log level for the CLI.
38
+
39
+ This can be overridden by the `GRIFFE_LOG_LEVEL` environment variable.
40
+ """
41
+
42
+
43
+ class _DebugInfo(argparse.Action):
44
+ def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None:
45
+ super().__init__(nargs=nargs, **kwargs)
46
+
47
+ def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
48
+ from griffe._internal import debug # noqa: PLC0415
49
+
50
+ debug._print_debug_info()
51
+ sys.exit(0)
52
+
53
+
54
+ def _print_data(data: str, output_file: str | IO | None) -> None:
55
+ if isinstance(output_file, str):
56
+ with open(output_file, "w", encoding="utf8") as fd: # noqa: PTH123
57
+ print(data, file=fd)
58
+ else:
59
+ if output_file is None:
60
+ output_file = sys.stdout
61
+ print(data, file=output_file)
62
+
63
+
64
+ def _load_packages(
65
+ packages: Sequence[str],
66
+ *,
67
+ extensions: Extensions | None = None,
68
+ search_paths: Sequence[str | Path] | None = None,
69
+ docstring_parser: DocstringStyle | Parser | None = None,
70
+ docstring_options: DocstringOptions | None = None,
71
+ resolve_aliases: bool = True,
72
+ resolve_implicit: bool = False,
73
+ resolve_external: bool | None = None,
74
+ allow_inspection: bool = True,
75
+ force_inspection: bool = False,
76
+ store_source: bool = True,
77
+ find_stubs_package: bool = False,
78
+ ) -> GriffeLoader:
79
+ from griffe._internal.loader import GriffeLoader # noqa: PLC0415
80
+ from griffe._internal.logger import logger # noqa: PLC0415
81
+
82
+ # Create a single loader.
83
+ loader = GriffeLoader(
84
+ extensions=extensions,
85
+ search_paths=search_paths,
86
+ docstring_parser=docstring_parser,
87
+ docstring_options=docstring_options,
88
+ allow_inspection=allow_inspection,
89
+ force_inspection=force_inspection,
90
+ store_source=store_source,
91
+ )
92
+
93
+ # Load each package.
94
+ for package in packages:
95
+ if not package:
96
+ logger.debug("Empty package name, continuing")
97
+ continue
98
+ logger.info("Loading package %s", package)
99
+ try:
100
+ loader.load(package, try_relative_path=True, find_stubs_package=find_stubs_package)
101
+ except ModuleNotFoundError as error:
102
+ logger.error("Could not find package %s: %s", package, error)
103
+ except ImportError:
104
+ logger.exception("Tried but could not import package %s", package)
105
+ logger.info("Finished loading packages")
106
+
107
+ # Resolve aliases.
108
+ if resolve_aliases:
109
+ logger.info("Starting alias resolution")
110
+ unresolved, iterations = loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external)
111
+ if unresolved:
112
+ logger.info("%s aliases were still unresolved after %s iterations", len(unresolved), iterations)
113
+ else:
114
+ logger.info("All aliases were resolved after %s iterations", iterations)
115
+ return loader
116
+
117
+
118
+ _level_choices = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
119
+
120
+
121
+ def _extensions_type(value: str) -> Sequence[str | dict[str, Any]]:
122
+ try:
123
+ return json.loads(value)
124
+ except json.JSONDecodeError:
125
+ return value.split(",")
126
+
127
+
128
+ def get_parser() -> argparse.ArgumentParser:
129
+ """Return the CLI argument parser.
130
+
131
+ Returns:
132
+ An argparse parser.
133
+ """
134
+ from griffe._internal import debug # noqa: PLC0415
135
+ from griffe._internal.enumerations import ExplanationStyle, Parser # noqa: PLC0415
136
+
137
+ usage = "%(prog)s [GLOBAL_OPTS...] COMMAND [COMMAND_OPTS...]"
138
+ description = "Signatures for entire Python programs. "
139
+ "Extract the structure, the frame, the skeleton of your project, "
140
+ "to generate API documentation or find breaking changes in your API."
141
+ parser = argparse.ArgumentParser(add_help=False, usage=usage, description=description, prog="griffe")
142
+
143
+ main_help = "Show this help message and exit. Commands also accept the -h/--help option."
144
+ subcommand_help = "Show this help message and exit."
145
+
146
+ global_options = parser.add_argument_group(title="Global options")
147
+ global_options.add_argument("-h", "--help", action="help", help=main_help)
148
+ global_options.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug._get_version()}")
149
+ global_options.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.")
150
+
151
+ def add_common_options(subparser: argparse.ArgumentParser) -> None:
152
+ common_options = subparser.add_argument_group(title="Common options")
153
+ common_options.add_argument("-h", "--help", action="help", help=subcommand_help)
154
+ search_options = subparser.add_argument_group(title="Search options")
155
+ search_options.add_argument(
156
+ "-s",
157
+ "--search",
158
+ dest="search_paths",
159
+ action="append",
160
+ type=Path,
161
+ help="Paths to search packages into.",
162
+ )
163
+ search_options.add_argument(
164
+ "-y",
165
+ "--sys-path",
166
+ dest="append_sys_path",
167
+ action="store_true",
168
+ help="Whether to append `sys.path` to search paths specified with `-s`.",
169
+ )
170
+ loading_options = subparser.add_argument_group(title="Loading options")
171
+ loading_options.add_argument(
172
+ "-B",
173
+ "--find-stubs-packages",
174
+ dest="find_stubs_package",
175
+ action="store_true",
176
+ default=False,
177
+ help="Whether to look for stubs-only packages and merge them with concrete ones.",
178
+ )
179
+ loading_options.add_argument(
180
+ "-e",
181
+ "--extensions",
182
+ default={},
183
+ type=_extensions_type,
184
+ help="A list of extensions to use.",
185
+ )
186
+ loading_options.add_argument(
187
+ "-X",
188
+ "--no-inspection",
189
+ dest="allow_inspection",
190
+ action="store_false",
191
+ default=True,
192
+ help="Disallow inspection of builtin/compiled/not found modules.",
193
+ )
194
+ loading_options.add_argument(
195
+ "-x",
196
+ "--force-inspection",
197
+ dest="force_inspection",
198
+ action="store_true",
199
+ default=False,
200
+ help="Force inspection of everything, even when sources are found.",
201
+ )
202
+ debug_options = subparser.add_argument_group(title="Debugging options")
203
+ debug_options.add_argument(
204
+ "-L",
205
+ "--log-level",
206
+ metavar="LEVEL",
207
+ default=DEFAULT_LOG_LEVEL,
208
+ choices=_level_choices,
209
+ type=str.upper,
210
+ help="Set the log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`.",
211
+ )
212
+
213
+ # ========= SUBPARSERS ========= #
214
+ subparsers = parser.add_subparsers(
215
+ dest="subcommand",
216
+ title="Commands",
217
+ metavar="COMMAND",
218
+ prog="griffe",
219
+ required=True,
220
+ )
221
+
222
+ def add_subparser(command: str, text: str, **kwargs: Any) -> argparse.ArgumentParser:
223
+ return subparsers.add_parser(command, add_help=False, help=text, description=text, **kwargs)
224
+
225
+ # ========= DUMP PARSER ========= #
226
+ dump_parser = add_subparser("dump", "Load package-signatures and dump them as JSON.")
227
+ dump_options = dump_parser.add_argument_group(title="Dump options")
228
+ dump_options.add_argument("packages", metavar="PACKAGE", nargs="+", help="Packages to find, load and dump.")
229
+ dump_options.add_argument(
230
+ "-f",
231
+ "--full",
232
+ action="store_true",
233
+ default=False,
234
+ help="Whether to dump full data in JSON.",
235
+ )
236
+ dump_options.add_argument(
237
+ "-o",
238
+ "--output",
239
+ default=sys.stdout,
240
+ help="Output file. Supports templating to output each package in its own file, with `{package}`.",
241
+ )
242
+ dump_options.add_argument(
243
+ "-d",
244
+ "--docstyle",
245
+ dest="docstring_parser",
246
+ default=None,
247
+ type=Parser,
248
+ help="The docstring style to parse.",
249
+ )
250
+ dump_options.add_argument(
251
+ "-D",
252
+ "--docopts",
253
+ dest="docstring_options",
254
+ default={},
255
+ type=json.loads,
256
+ help="The options for the docstring parser.",
257
+ )
258
+ dump_options.add_argument(
259
+ "-r",
260
+ "--resolve-aliases",
261
+ action="store_true",
262
+ help="Whether to resolve aliases.",
263
+ )
264
+ dump_options.add_argument(
265
+ "-I",
266
+ "--resolve-implicit",
267
+ action="store_true",
268
+ help="Whether to resolve implicitly exported aliases as well. "
269
+ "Aliases are explicitly exported when defined in `__all__`.",
270
+ )
271
+ dump_options.add_argument(
272
+ "-U",
273
+ "--resolve-external",
274
+ dest="resolve_external",
275
+ action="store_true",
276
+ help="Always resolve aliases pointing to external/unknown modules (not loaded directly)."
277
+ "Default is to resolve only from one module to its private sibling (`ast` -> `_ast`).",
278
+ )
279
+ dump_options.add_argument(
280
+ "--no-resolve-external",
281
+ dest="resolve_external",
282
+ action="store_false",
283
+ help="Never resolve aliases pointing to external/unknown modules (not loaded directly)."
284
+ "Default is to resolve only from one module to its private sibling (`ast` -> `_ast`).",
285
+ )
286
+ dump_options.add_argument(
287
+ "-S",
288
+ "--stats",
289
+ action="store_true",
290
+ help="Show statistics at the end.",
291
+ )
292
+ add_common_options(dump_parser)
293
+
294
+ # ========= CHECK PARSER ========= #
295
+ check_parser = add_subparser("check", "Check for API breakages or possible improvements.")
296
+ check_options = check_parser.add_argument_group(title="Check options")
297
+ check_options.add_argument("package", metavar="PACKAGE", help="Package to find, load and check, as path.")
298
+ check_options.add_argument(
299
+ "-a",
300
+ "--against",
301
+ metavar="REF",
302
+ help="Older Git reference (commit, branch, tag) to check against. Default: load latest tag.",
303
+ )
304
+ check_options.add_argument(
305
+ "-b",
306
+ "--base-ref",
307
+ metavar="BASE_REF",
308
+ help="Git reference (commit, branch, tag) to check. Default: load current code.",
309
+ )
310
+ check_options.add_argument(
311
+ "--color",
312
+ dest="color",
313
+ action="store_true",
314
+ default=None,
315
+ help="Force enable colors in the output.",
316
+ )
317
+ check_options.add_argument(
318
+ "--no-color",
319
+ dest="color",
320
+ action="store_false",
321
+ default=None,
322
+ help="Force disable colors in the output.",
323
+ )
324
+ check_options.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
325
+ formats = [fmt.value for fmt in ExplanationStyle]
326
+ check_options.add_argument("-f", "--format", dest="style", choices=formats, default=None, help="Output format.")
327
+ add_common_options(check_parser)
328
+
329
+ return parser
330
+
331
+
332
+ def dump(
333
+ packages: Sequence[str],
334
+ *,
335
+ output: str | IO | None = None,
336
+ full: bool = False,
337
+ docstring_parser: DocstringStyle | Parser | None = None,
338
+ docstring_options: DocstringOptions | None = None,
339
+ extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
340
+ resolve_aliases: bool = False,
341
+ resolve_implicit: bool = False,
342
+ resolve_external: bool | None = None,
343
+ search_paths: Sequence[str | Path] | None = None,
344
+ find_stubs_package: bool = False,
345
+ append_sys_path: bool = False,
346
+ allow_inspection: bool = True,
347
+ force_inspection: bool = False,
348
+ stats: bool = False,
349
+ ) -> int:
350
+ """Load packages data and dump it as JSON.
351
+
352
+ Parameters:
353
+ packages: The packages to load and dump.
354
+ output: Where to output the JSON-serialized data.
355
+ full: Whether to output full or minimal data.
356
+ docstring_parser: The docstring parser to use. By default, no parsing is done.
357
+ docstring_options: Docstring parsing options.
358
+ resolve_aliases: Whether to resolve aliases (indirect objects references).
359
+ resolve_implicit: Whether to resolve every alias or only the explicitly exported ones.
360
+ resolve_external: Whether to load additional, unspecified modules to resolve aliases.
361
+ Default is to resolve only from one module to its private sibling (`ast` -> `_ast`).
362
+ extensions: The extensions to use.
363
+ search_paths: The paths to search into.
364
+ find_stubs_package: Whether to search for stubs-only packages.
365
+ If both the package and its stubs are found, they'll be merged together.
366
+ If only the stubs are found, they'll be used as the package itself.
367
+ append_sys_path: Whether to append the contents of `sys.path` to the search paths.
368
+ allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
369
+ force_inspection: Whether to force using dynamic analysis when loading data.
370
+ stats: Whether to compute and log stats about loading.
371
+
372
+ Returns:
373
+ `0` for success, `1` for failure.
374
+ """
375
+ from griffe._internal.encoders import JSONEncoder # noqa: PLC0415
376
+ from griffe._internal.exceptions import ExtensionError # noqa: PLC0415
377
+ from griffe._internal.extensions.base import load_extensions # noqa: PLC0415
378
+ from griffe._internal.logger import logger # noqa: PLC0415
379
+
380
+ # Prepare options.
381
+ per_package_output = False
382
+ if isinstance(output, str) and output.format(package="package") != output:
383
+ per_package_output = True
384
+
385
+ search_paths = list(search_paths) if search_paths else []
386
+ if append_sys_path:
387
+ search_paths.extend(sys.path)
388
+
389
+ try:
390
+ loaded_extensions = load_extensions(*(extensions or ()))
391
+ except ExtensionError:
392
+ logger.exception("Could not load extensions")
393
+ return 1
394
+
395
+ # Load packages.
396
+ loader = _load_packages(
397
+ packages,
398
+ extensions=loaded_extensions,
399
+ search_paths=search_paths,
400
+ docstring_parser=docstring_parser,
401
+ docstring_options=docstring_options,
402
+ resolve_aliases=resolve_aliases,
403
+ resolve_implicit=resolve_implicit,
404
+ resolve_external=resolve_external,
405
+ allow_inspection=allow_inspection,
406
+ force_inspection=force_inspection,
407
+ store_source=False,
408
+ find_stubs_package=find_stubs_package,
409
+ )
410
+ data_packages = loader.modules_collection.members
411
+
412
+ # Serialize and dump packages.
413
+ started = datetime.now(tz=timezone.utc)
414
+ if per_package_output:
415
+ for package_name, data in data_packages.items():
416
+ serialized = data.as_json(indent=2, full=full, sort_keys=True)
417
+ _print_data(serialized, output.format(package=package_name)) # type: ignore[union-attr]
418
+ else:
419
+ serialized = json.dumps(data_packages, cls=JSONEncoder, indent=2, full=full, sort_keys=True)
420
+ _print_data(serialized, output)
421
+ elapsed = datetime.now(tz=timezone.utc) - started
422
+
423
+ if stats:
424
+ loader_stats = loader.stats()
425
+ loader_stats.time_spent_serializing = elapsed.microseconds
426
+ logger.info(loader_stats.as_text())
427
+
428
+ return 0 if len(data_packages) == len(packages) else 1
429
+
430
+
431
+ def check(
432
+ package: str | Path,
433
+ against: str | None = None,
434
+ against_path: str | Path | None = None,
435
+ *,
436
+ base_ref: str | None = None,
437
+ extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None,
438
+ search_paths: Sequence[str | Path] | None = None,
439
+ append_sys_path: bool = False,
440
+ find_stubs_package: bool = False,
441
+ allow_inspection: bool = True,
442
+ force_inspection: bool = False,
443
+ verbose: bool = False,
444
+ color: bool | None = None,
445
+ style: str | ExplanationStyle | None = None,
446
+ ) -> int:
447
+ """Check for API breaking changes in two versions of the same package.
448
+
449
+ Parameters:
450
+ package: The package to load and check.
451
+ against: Older Git reference (commit, branch, tag) to check against.
452
+ against_path: Path when the "against" reference is checked out.
453
+ base_ref: Git reference (commit, branch, tag) to check.
454
+ extensions: The extensions to use.
455
+ search_paths: The paths to search into.
456
+ append_sys_path: Whether to append the contents of `sys.path` to the search paths.
457
+ allow_inspection: Whether to allow inspecting modules when visiting them is not possible.
458
+ force_inspection: Whether to force using dynamic analysis when loading data.
459
+ verbose: Use a verbose output.
460
+
461
+ Returns:
462
+ `0` for success, `1` for failure.
463
+ """
464
+ from griffe._internal.diff import find_breaking_changes # noqa: PLC0415
465
+ from griffe._internal.enumerations import ExplanationStyle # noqa: PLC0415
466
+ from griffe._internal.exceptions import ExtensionError, GitError # noqa: PLC0415
467
+ from griffe._internal.extensions.base import load_extensions # noqa: PLC0415
468
+ from griffe._internal.git import _get_latest_tag, _get_repo_root # noqa: PLC0415
469
+ from griffe._internal.loader import load, load_git, load_pypi # noqa: PLC0415
470
+ from griffe._internal.logger import logger # noqa: PLC0415
471
+
472
+ # Prepare options.
473
+ search_paths = list(search_paths) if search_paths else []
474
+ if append_sys_path:
475
+ search_paths.extend(sys.path)
476
+
477
+ against_path = against_path or package
478
+ try:
479
+ loaded_extensions = load_extensions(*(extensions or ()))
480
+ except ExtensionError:
481
+ logger.exception("Could not load extensions")
482
+ return 1
483
+
484
+ if match_against := re.match(r"([\w.-]+)?((==|<=|<|>=|>|!=).+)", against or ""):
485
+ against_dist = (match_against.group(1) or str(package)).lower().replace("-", "_")
486
+ against_version = match_against.group(2)
487
+ old_package = load_pypi(
488
+ str(package),
489
+ against_dist,
490
+ against_version,
491
+ extensions=loaded_extensions,
492
+ search_paths=search_paths,
493
+ allow_inspection=allow_inspection,
494
+ force_inspection=force_inspection,
495
+ find_stubs_package=find_stubs_package,
496
+ resolve_aliases=True,
497
+ resolve_external=None,
498
+ )
499
+
500
+ if base_ref:
501
+ if not (match_base := re.match(r"([\w.-]+)?((==|<=|<|>=|>|!=).+)", base_ref)):
502
+ raise ValueError(f"Base {base_ref} is not a valid dependency specifier.")
503
+ base_dist = (match_base.group(1) or str(package)).lower().replace("-", "_")
504
+ base_version = match_base.group(2)
505
+ else:
506
+ base_dist = against_dist
507
+ base_version = ""
508
+ new_package = load_pypi(
509
+ str(package),
510
+ base_dist,
511
+ base_version,
512
+ extensions=loaded_extensions,
513
+ search_paths=search_paths,
514
+ allow_inspection=allow_inspection,
515
+ force_inspection=force_inspection,
516
+ find_stubs_package=find_stubs_package,
517
+ resolve_aliases=True,
518
+ resolve_external=None,
519
+ )
520
+
521
+ else:
522
+ against_path = against_path or package
523
+ try:
524
+ against = against or _get_latest_tag(package)
525
+ repository = _get_repo_root(against_path)
526
+ except GitError as error:
527
+ print(f"griffe: error: {error}", file=sys.stderr)
528
+ return 2
529
+
530
+ # Load old and new version of the package.
531
+ old_package = load_git(
532
+ against_path,
533
+ ref=against,
534
+ repo=repository,
535
+ extensions=loaded_extensions,
536
+ search_paths=search_paths,
537
+ allow_inspection=allow_inspection,
538
+ force_inspection=force_inspection,
539
+ find_stubs_package=find_stubs_package,
540
+ resolve_aliases=True,
541
+ resolve_external=None,
542
+ )
543
+
544
+ if base_ref:
545
+ new_package = load_git(
546
+ package,
547
+ ref=base_ref,
548
+ repo=repository,
549
+ extensions=loaded_extensions,
550
+ search_paths=search_paths,
551
+ allow_inspection=allow_inspection,
552
+ force_inspection=force_inspection,
553
+ find_stubs_package=find_stubs_package,
554
+ resolve_aliases=True,
555
+ resolve_external=None,
556
+ )
557
+ else:
558
+ new_package = load(
559
+ package,
560
+ try_relative_path=True,
561
+ extensions=loaded_extensions,
562
+ search_paths=search_paths,
563
+ allow_inspection=allow_inspection,
564
+ force_inspection=force_inspection,
565
+ find_stubs_package=find_stubs_package,
566
+ resolve_aliases=True,
567
+ resolve_external=None,
568
+ )
569
+
570
+ # Find and display API breakages.
571
+ breakages = list(find_breaking_changes(old_package, new_package))
572
+
573
+ if color is None and (force_color := os.getenv("FORCE_COLOR", None)) is not None:
574
+ color = force_color.lower() in {"1", "true", "y", "yes", "on"}
575
+ colorama.deinit()
576
+ colorama.init(strip=color if color is None else not color)
577
+
578
+ if style is None:
579
+ style = ExplanationStyle.VERBOSE if verbose else ExplanationStyle.ONE_LINE
580
+ else:
581
+ style = ExplanationStyle(style)
582
+ for breakage in breakages:
583
+ print(breakage.explain(style=style), file=sys.stderr)
584
+
585
+ if breakages:
586
+ return 1
587
+ return 0
588
+
589
+
590
+ def main(args: list[str] | None = None) -> int:
591
+ """Run the main program.
592
+
593
+ This function is executed when you type `griffe` or `python -m griffe`.
594
+
595
+ Parameters:
596
+ args: Arguments passed from the command line.
597
+
598
+ Returns:
599
+ An exit code.
600
+ """
601
+ # Parse arguments.
602
+ parser = get_parser()
603
+ opts: argparse.Namespace = parser.parse_args(args)
604
+ opts_dict = opts.__dict__
605
+ opts_dict.pop("debug_info")
606
+ subcommand = opts_dict.pop("subcommand")
607
+
608
+ # Initialize logging.
609
+ log_level = opts_dict.pop("log_level", DEFAULT_LOG_LEVEL)
610
+ try:
611
+ level = getattr(logging, log_level)
612
+ except AttributeError:
613
+ choices = "', '".join(_level_choices)
614
+ print(
615
+ f"griffe: error: invalid log level '{log_level}' (choose from '{choices}')",
616
+ file=sys.stderr,
617
+ )
618
+ return 1
619
+ else:
620
+ logging.basicConfig(format="%(levelname)-10s %(message)s", level=level)
621
+
622
+ # Increase maximum recursion limit to 2000.
623
+ sys.setrecursionlimit(max(2000, sys.getrecursionlimit()))
624
+
625
+ # Run subcommand.
626
+ commands: dict[str, Callable[..., int]] = {"check": check, "dump": dump}
627
+ return commands[subcommand](**opts_dict)
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: griffecli
3
+ Version: 2.0.0
4
+ Summary: Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API.
5
+ Author-email: Timothée Mazzucotelli <dev@pawamoy.fr>
6
+ License-Expression: ISC
7
+ License-File: LICENSE
8
+ Keywords: api,breaking-changes,dynamic-analysis,signature,static-analysis
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Documentation
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: Software Development :: Documentation
22
+ Classifier: Topic :: Utilities
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: colorama>=0.4
26
+ Requires-Dist: griffelib==2.0.0
@@ -0,0 +1,9 @@
1
+ griffecli/__init__.py,sha256=z-Nvz0xOow5LcvLTvWQFJQJGPokZvg-Eu3Zzds44RH0,773
2
+ griffecli/__main__.py,sha256=5LZTL_F1h9Sb9q_1Fdv24RNbLN6aYb9_oteInbPPWRg,356
3
+ griffecli/_internal/__init__.py,sha256=1wv-xg5VvTNTPQ-rgaFFIqPlTW8_jtb_O5j8ZORjVWM,46
4
+ griffecli/_internal/cli.py,sha256=MyRPRjVQfJ6W_NThJksGkMFn1lh9Xy3A7lfBoxBW9E8,23492
5
+ griffecli-2.0.0.dist-info/METADATA,sha256=5KLk-QowvVwJCyXR1fPYo0LDF0N0_tV0n0Ilb44IIww,1189
6
+ griffecli-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ griffecli-2.0.0.dist-info/entry_points.txt,sha256=sYL4YV7-lVHVQoBf3ObvyoaVDP9HU1tnZKvBAzBKWnQ,45
8
+ griffecli-2.0.0.dist-info/licenses/LICENSE,sha256=JGb4pdPEM8TTjjhr-uNCO7oXkiVrwG5Pz0JamcjNF_s,754
9
+ griffecli-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ griffecli = griffecli:main
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2021, Timothée Mazzucotelli
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.