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 +27 -0
- griffecli/__main__.py +13 -0
- griffecli/_internal/__init__.py +1 -0
- griffecli/_internal/cli.py +627 -0
- griffecli-2.0.0.dist-info/METADATA +26 -0
- griffecli-2.0.0.dist-info/RECORD +9 -0
- griffecli-2.0.0.dist-info/WHEEL +4 -0
- griffecli-2.0.0.dist-info/entry_points.txt +2 -0
- griffecli-2.0.0.dist-info/licenses/LICENSE +15 -0
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,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.
|