docsig 0.81.2__tar.gz → 0.82.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docsig
3
- Version: 0.81.2
3
+ Version: 0.82.0
4
4
  Summary: Check signature params for proper documentation
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -184,7 +184,7 @@ ensure your installation has registered `docsig`
184
184
  .. code-block:: console
185
185
 
186
186
  $ flake8 --version
187
- 7.3.0 (docsig: 0.81.2, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
187
+ 7.3.0 (docsig: 0.82.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
188
188
 
189
189
  And now use `flake8` to lint your files
190
190
 
@@ -270,7 +270,7 @@ Standalone
270
270
 
271
271
  repos:
272
272
  - repo: https://github.com/jshwi/docsig
273
- rev: v0.81.2
273
+ rev: v0.82.0
274
274
  hooks:
275
275
  - id: docsig
276
276
  args:
@@ -289,7 +289,7 @@ or integrated with ``flake8``
289
289
  hooks:
290
290
  - id: flake8
291
291
  additional_dependencies:
292
- - docsig==0.81.2
292
+ - docsig==0.82.0
293
293
  args:
294
294
  - "--sig-check-class"
295
295
  - "--sig-check-dunders"
@@ -155,7 +155,7 @@ ensure your installation has registered `docsig`
155
155
  .. code-block:: console
156
156
 
157
157
  $ flake8 --version
158
- 7.3.0 (docsig: 0.81.2, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
158
+ 7.3.0 (docsig: 0.82.0, mccabe: 0.7.0, pycodestyle: 2.14.0, pyflakes: 3.4.0) CPython 3.10.19 on Darwin
159
159
 
160
160
  And now use `flake8` to lint your files
161
161
 
@@ -241,7 +241,7 @@ Standalone
241
241
 
242
242
  repos:
243
243
  - repo: https://github.com/jshwi/docsig
244
- rev: v0.81.2
244
+ rev: v0.82.0
245
245
  hooks:
246
246
  - id: docsig
247
247
  args:
@@ -260,7 +260,7 @@ or integrated with ``flake8``
260
260
  hooks:
261
261
  - id: flake8
262
262
  additional_dependencies:
263
- - docsig==0.81.2
263
+ - docsig==0.82.0
264
264
  args:
265
265
  - "--sig-check-class"
266
266
  - "--sig-check-dunders"
@@ -0,0 +1,82 @@
1
+ """
2
+ docsig._check
3
+ =============
4
+ """
5
+
6
+ from __future__ import annotations as _
7
+
8
+ from ._config import Config as _Config
9
+ from ._module import Function as _Function
10
+ from ._module import Parent as _Parent
11
+ from ._report import Failure as _Failure
12
+ from ._report import Failures as _Failures
13
+
14
+
15
+ def _should_check_function(
16
+ function: _Function,
17
+ parent: _Parent,
18
+ config: _Config,
19
+ ) -> bool:
20
+ if function.isoverridden and not config.check.overridden:
21
+ return False
22
+
23
+ if function.isprotected and not config.check.protected:
24
+ return False
25
+
26
+ if function.isdunder and not config.check.dunders:
27
+ return False
28
+
29
+ if function.docstring.bare and config.ignore.no_params:
30
+ return False
31
+
32
+ if function.isinit and (
33
+ not (config.check.class_ or config.check.class_constructor)
34
+ or (parent.isprotected and not config.check.protected)
35
+ ):
36
+ return False
37
+
38
+ return True
39
+
40
+
41
+ def _run_check(
42
+ child: _Parent,
43
+ parent: _Parent,
44
+ config: _Config,
45
+ failures: _Failures,
46
+ ) -> None:
47
+ if isinstance(child, _Function) and _should_check_function(
48
+ child,
49
+ parent,
50
+ config,
51
+ ):
52
+ failure = _Failure(child, config)
53
+ if failure:
54
+ failures.append(failure)
55
+
56
+ # recurse for either class methods or, if enabled, nested functions
57
+ if not isinstance(child, _Function) or config.check.nested:
58
+ for child_of_child in child.children:
59
+ _run_check(child_of_child, child, config, failures)
60
+
61
+
62
+ def run_checks(module: _Parent, config: _Config) -> _Failures:
63
+ """Run checks on the module and return a list of failures.
64
+
65
+ Traverse the module's functions and classes. A callable is checked
66
+ only if it is public or if configuration allows protected or
67
+ overridden members. Failures are collected and returned.
68
+
69
+ :param module: A module object that contains functions or classes.
70
+ :param config: Configuration object.
71
+ :return: A list of function and class failures.
72
+ """
73
+ failures = _Failures()
74
+ for child in module.children:
75
+ if (
76
+ not child.isprotected
77
+ or config.check.protected
78
+ or config.check.protected_class_methods
79
+ ):
80
+ _run_check(child, module, config, failures)
81
+
82
+ return failures
@@ -18,7 +18,7 @@ from pathlib import Path as _Path
18
18
  import tomli as _tomli
19
19
 
20
20
  from ._version import __version__
21
- from .messages import Messages
21
+ from .messages import Messages as _Messages
22
22
 
23
23
  PYPROJECT_TOML = "pyproject.toml"
24
24
 
@@ -361,8 +361,8 @@ class Config:
361
361
 
362
362
  check: Check = _field(default_factory=Check)
363
363
  ignore: Ignore = _field(default_factory=Ignore)
364
- target: Messages = _field(default_factory=Messages)
365
- disable: Messages = _field(default_factory=Messages)
364
+ target: _Messages = _field(default_factory=_Messages)
365
+ disable: _Messages = _field(default_factory=_Messages)
366
366
  exclude: list[str] = _field(default_factory=list)
367
367
  excludes: list[str] | None = None
368
368
  list_checks: bool = False
@@ -8,30 +8,21 @@ Entry point and orchestration for running docstring/signature checks.
8
8
  from __future__ import annotations as _
9
9
 
10
10
  import logging as _logging
11
- import os as _os
12
11
  import sys as _sys
13
12
  import warnings as _warnings
14
13
  from pathlib import Path as _Path
15
- from tokenize import TokenError as _TokenError
16
- from warnings import warn as _warn
17
-
18
- import astroid as _ast
19
14
 
20
15
  from . import _decorators
16
+ from ._check import run_checks as _run_checks
21
17
  from ._config import Check as _Check
22
18
  from ._config import Config as _Config
23
19
  from ._config import Ignore as _Ignore
24
- from ._directives import Directives as _Directives
25
- from ._files import FILE_INFO as _FILE_INFO
26
- from ._files import Paths as _Paths
27
- from ._module import Error as _Error
28
- from ._module import Function as _Function
29
- from ._module import Parent as _Parent
30
- from ._report import Failure as _Failure
20
+ from ._files import Files as _Files
21
+ from ._parsers import parse_from_file as _parse_from_file
22
+ from ._parsers import parse_from_string as _parse_from_string
31
23
  from ._report import Failures as _Failures
24
+ from ._report import report as _report
32
25
  from ._utils import print_checks as _print_checks
33
- from .messages import NEW as _NEW
34
- from .messages import TEMPLATE as _TEMPLATE
35
26
  from .messages import E as _E
36
27
  from .messages import Messages as _Messages
37
28
 
@@ -70,159 +61,6 @@ def setup_logger(verbose: bool) -> None:
70
61
  logger.addHandler(stream_handler)
71
62
 
72
63
 
73
- def _should_check_function(
74
- child: _Function,
75
- parent: _Parent,
76
- config: _Config,
77
- ) -> bool:
78
- if child.isoverridden and not config.check.overridden:
79
- return False
80
-
81
- if child.isprotected and not config.check.protected:
82
- return False
83
-
84
- if child.isdunder and not config.check.dunders:
85
- return False
86
-
87
- if child.docstring.bare and config.ignore.no_params:
88
- return False
89
-
90
- if child.isinit and (
91
- not (config.check.class_ or config.check.class_constructor)
92
- or (parent.isprotected and not config.check.protected)
93
- ):
94
- return False
95
-
96
- return True
97
-
98
-
99
- def _run_check(
100
- child: _Parent,
101
- parent: _Parent,
102
- config: _Config,
103
- failures: _Failures,
104
- ) -> None:
105
- if isinstance(child, _Function) and _should_check_function(
106
- child,
107
- parent,
108
- config,
109
- ):
110
- failure = _Failure(child, config.target, config.check.property_returns)
111
- if failure:
112
- failures.append(failure)
113
-
114
- # recurse for either class methods or, if enabled, nested functions
115
- if not isinstance(child, _Function) or config.check.nested:
116
- for func in child.children:
117
- _run_check(func, child, config, failures)
118
-
119
-
120
- def _from_file(path: _Path, config: _Config) -> _Parent:
121
- try:
122
- code = path.read_text(encoding="utf-8")
123
- parent = _from_str(
124
- code,
125
- config,
126
- str(path)[:-3].replace(_os.sep, ".").replace("-", "_"),
127
- path,
128
- )
129
- except UnicodeDecodeError as err:
130
- logger = _logging.getLogger(__package__)
131
- logger.debug(_FILE_INFO, path, str(err).replace("\n", " "))
132
- parent = _Parent(error=_Error.UNICODE)
133
-
134
- if parent.error is not None and not path.name.endswith(".py"):
135
- parent = _Parent()
136
-
137
- return parent
138
-
139
-
140
- def _from_str(
141
- code: str,
142
- config: _Config,
143
- module_name: str = "",
144
- path: _Path | None = None,
145
- ) -> _Parent:
146
- logger = _logging.getLogger(__package__)
147
- source_name = path or "stdin"
148
- try:
149
- node = _ast.parse(code, module_name, str(path))
150
- try:
151
- directives = _Directives.from_text(code, config.disable)
152
- except _TokenError as err:
153
- directives = _Directives()
154
- logger.debug(
155
- _FILE_INFO,
156
- source_name,
157
- f"error parsing comments {err}".lower(),
158
- )
159
-
160
- parent = _Parent(
161
- node,
162
- directives,
163
- path,
164
- config.ignore.args,
165
- config.ignore.kwargs,
166
- config.check.class_constructor,
167
- )
168
- logger.debug(_FILE_INFO, source_name, "Parsing Python code successful")
169
- except _ast.AstroidSyntaxError as err:
170
- logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
171
- parent = _Parent(error=_Error.SYNTAX)
172
-
173
- return parent
174
-
175
-
176
- def _get_failures(module: _Parent, config: _Config) -> _Failures:
177
- failures = _Failures()
178
- for top_level in module.children:
179
- if (
180
- not top_level.isprotected
181
- or config.check.protected
182
- or config.check.protected_class_methods
183
- ):
184
- _run_check(top_level, module, config, failures)
185
-
186
- return failures
187
-
188
-
189
- def _report(
190
- failures: _Failures,
191
- config: _Config,
192
- path: str | None = None,
193
- ) -> int:
194
- retcodes = [0]
195
- for failure in failures:
196
- retcodes.append(failure.retcode)
197
- path_prefix = f"{path}:" if path is not None else ""
198
- header = f"{path_prefix}{failure.lineno} in {failure.name}"
199
- if not config.no_ansi and _sys.stdout.isatty():
200
- header = f"\033[35m{header}\033[0m"
201
-
202
- print(header)
203
- for item in failure:
204
- extra = None
205
- if item.hint:
206
- extra = f"hint: {item.hint}"
207
-
208
- if item.new:
209
- extra = "warning: please remember to fix this or disable it"
210
- _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
211
-
212
- print(
213
- " "
214
- + _TEMPLATE.format(
215
- ref=item.ref,
216
- description=item.description,
217
- symbolic=item.symbolic,
218
- ),
219
- )
220
- if extra is not None:
221
- print(f" {extra}")
222
-
223
- return max(retcodes)
224
-
225
-
226
64
  def handle_deprecations(
227
65
  ignore_typechecker: bool,
228
66
  disable: list,
@@ -245,15 +83,15 @@ def handle_deprecations(
245
83
  disable.extend(messages)
246
84
 
247
85
 
248
- def runner(path: _Path, config: _Config) -> _Failures:
86
+ def runner(file: _Path, config: _Config) -> _Failures:
249
87
  """Run checks for a single file and return collected failures.
250
88
 
251
- :param path: Path to the file to check.
89
+ :param file: Path to the file to check.
252
90
  :param config: Configuration object.
253
91
  :return: Collected failures for the file.
254
92
  """
255
- module = _from_file(path, config)
256
- return _get_failures(module, config)
93
+ module = _parse_from_file(file, config)
94
+ return _run_checks(module, config)
257
95
 
258
96
 
259
97
  @_decorators.parse_msgs
@@ -367,20 +205,16 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments
367
205
  if config.list_checks:
368
206
  return int(bool(_print_checks())) # type: ignore
369
207
 
370
- if string is None:
371
- retcodes = [0]
372
- paths = _Paths(
373
- *path,
374
- patterns=config.exclude,
375
- excludes=config.excludes,
376
- include_ignored=config.include_ignored,
377
- )
378
- for path_ in paths:
379
- failures = runner(path_, config)
380
- retcodes.append(_report(failures, config, str(path_)))
208
+ if string:
209
+ module = _parse_from_string(string, config)
210
+ failures = _run_checks(module, config)
211
+ return _report(failures, config)
381
212
 
382
- return max(retcodes)
213
+ retcodes = [0]
214
+ files = _Files(path, config)
215
+ for file in files:
216
+ failures = runner(file, config)
217
+ retcode = _report(failures, config, str(file))
218
+ retcodes.append(retcode)
383
219
 
384
- module = _from_str(string, config)
385
- failures = _get_failures(module, config)
386
- return _report(failures, config)
220
+ return max(retcodes)
@@ -15,6 +15,10 @@ from .messages import E as _E
15
15
  from .messages import Messages as _Messages
16
16
 
17
17
 
18
+ class Comments(_t.List["Comment"]):
19
+ """List of comments."""
20
+
21
+
18
22
  class Comment(_Messages):
19
23
  """A single comment directive.
20
24
 
@@ -23,12 +27,16 @@ class Comment(_Messages):
23
27
  """
24
28
 
25
29
  _valid_kinds = "enable", "disable"
30
+ _valid_flags = ("next",)
26
31
 
27
32
  def __init__(self, string: str, col: int) -> None:
28
33
  super().__init__()
29
34
  self._ismodule = col == 0
30
35
  parts = string.split("=")
31
- self._kind = parts[0]
36
+ subparts = parts[0].split("-")
37
+ self._kind = subparts[0]
38
+ self._flag = None if len(subparts) == 1 else subparts[1]
39
+ self._isnext = self._flag == "next"
32
40
  if len(parts) == 1:
33
41
  self.extend(_E.all)
34
42
  else:
@@ -59,6 +67,21 @@ class Comment(_Messages):
59
67
  """Whether this is a disable directive."""
60
68
  return self._kind == self._valid_kinds[1]
61
69
 
70
+ @property
71
+ def isnext(self) -> bool:
72
+ """Whether this directive applies to the next line only."""
73
+ return self._isnext
74
+
75
+ @property
76
+ def isvalidflag(self) -> bool:
77
+ """Whether the directive flag (if any) is valid."""
78
+ return self._flag is None or self._flag in self._valid_flags
79
+
80
+ @property
81
+ def flag(self) -> str | None:
82
+ """Flag for this directive (e.g. next), or None."""
83
+ return self._flag
84
+
62
85
  @classmethod
63
86
  def parse(cls, comment: str, col: int) -> Comment | None:
64
87
  """Parse a comment into a docsig directive when present.
@@ -73,10 +96,6 @@ class Comment(_Messages):
73
96
  return None
74
97
 
75
98
 
76
- class Comments(_t.List[Comment]):
77
- """List of comments."""
78
-
79
-
80
99
  class Directives(_t.Dict[int, _t.Tuple[Comments, _Messages]]):
81
100
  """Map line number to comments and disabled messages for that line.
82
101
 
@@ -96,6 +115,9 @@ class Directives(_t.Dict[int, _t.Tuple[Comments, _Messages]]):
96
115
  directives = cls()
97
116
  fin = _StringIO(text)
98
117
  comments = Comments()
118
+ enable_next = False
119
+ next_comments = Comments(comments)
120
+ next_messages = _Messages(messages)
99
121
  for line in _tokenize.generate_tokens(fin.readline):
100
122
  # do nothing for these line types
101
123
  if line.type in (_tokenize.NAME, _tokenize.OP, _tokenize.DEDENT):
@@ -119,12 +141,30 @@ class Directives(_t.Dict[int, _t.Tuple[Comments, _Messages]]):
119
141
  i for i in messages if i not in comment
120
142
  )
121
143
 
144
+ # if directive has a 'next' flag then alter the
145
+ # module level scope, not the inline scope (which
146
+ # behaves similar to next), so that inline
147
+ # directives are included and not overwritten
148
+ if comment.isnext and not enable_next:
149
+ enable_next = True
150
+ next_comments = Comments(comments)
151
+ next_messages = _Messages(messages)
152
+
122
153
  # if module level directive, then make changes
123
154
  # globally
124
155
  if comment.ismodule:
125
156
  comments = scoped_comments
126
157
  messages = scoped_messages
127
158
 
159
+ # if in a 'next' module level scope and the line type is a
160
+ # newline (not a comment to allow 'next' directives to be
161
+ # stacked) then leave the 'next' module level scope and
162
+ # reset the global messages to before the 'next' directive
163
+ elif enable_next and line.type != _tokenize.NL:
164
+ enable_next = False
165
+ comments = next_comments
166
+ messages = next_messages
167
+
128
168
  # check that a scoped message has not updated this first, as
129
169
  # they take precedence over global messages
130
170
  if lineno not in directives:
@@ -17,6 +17,8 @@ from pathspec import PathSpec as _PathSpec
17
17
  from pathspec.patterns import GitWildMatchPattern as _GitWildMatchPattern
18
18
  from wcmatch.pathlib import Path as _WcPath
19
19
 
20
+ from ._config import Config as _Config
21
+
20
22
  FILE_INFO = "%s: %s"
21
23
 
22
24
 
@@ -69,55 +71,46 @@ def _glob(path: _Path, pattern: str) -> bool:
69
71
  return _WcPath(str(path)).globmatch(pattern) # type: ignore
70
72
 
71
73
 
72
- class Paths(_t.List[_Path]):
74
+ class Files(_t.List[_Path]):
73
75
  """Collect paths to check (gitignore and exclude applied).
74
76
 
75
77
  :param paths: Path(s) to collect (files or directories).
76
- :param patterns: List pf regular expression of files and dirs to
77
- exclude from checks.
78
- :param excludes: Files or dirs to exclude from checks.
79
- :param include_ignored: Check files even if they match a gitignore
80
- pattern.
78
+ :param config: Configuration object.
81
79
  """
82
80
 
83
81
  def __init__(
84
82
  self,
85
- *paths: _Path | str,
86
- patterns: list[str],
87
- excludes: list[str] | None = None,
88
- include_ignored: bool = False,
83
+ paths: tuple[str | _Path, ...],
84
+ config: _Config,
89
85
  ) -> None:
90
86
  super().__init__()
91
- self._include_ignored = include_ignored
87
+ self._include_ignored = config.include_ignored
92
88
  self._gitignore = _Gitignore()
93
- self._logger = _logging.getLogger(__package__)
89
+ logger = _logging.getLogger(__package__)
94
90
  for path in paths:
95
91
  self._populate(_Path(path))
96
92
 
97
93
  for path in list(self):
98
94
  if str(path) != "." and (
99
- any(_re.match(i, str(path)) for i in patterns)
100
- or any(_glob(path, i) for i in excludes or [])
95
+ any(_re.match(i, str(path)) for i in config.exclude)
96
+ or any(_glob(path, i) for i in config.excludes or [])
101
97
  ):
102
- self._logger.debug(
103
- FILE_INFO,
104
- path,
105
- "in exclude list, skipping",
106
- )
98
+ logger.debug(FILE_INFO, path, "in exclude list, skipping")
107
99
  self.remove(path)
108
100
 
109
101
  self.sort()
110
102
 
111
103
  def _populate(self, root: _Path) -> None:
104
+ logger = _logging.getLogger(__package__)
112
105
  if not root.exists():
113
106
  if root.is_symlink():
114
- self._logger.debug(FILE_INFO, root, "broken link, skipping")
107
+ logger.debug(FILE_INFO, root, "broken link, skipping")
115
108
  return
116
109
 
117
110
  raise FileNotFoundError(root)
118
111
 
119
112
  if not self._include_ignored and self._gitignore.match_file(root):
120
- self._logger.debug(FILE_INFO, root, "in gitignore, skipping")
113
+ logger.debug(FILE_INFO, root, "in gitignore, skipping")
121
114
  return
122
115
 
123
116
  if root.is_file():
@@ -14,6 +14,7 @@ from pathlib import Path as _Path
14
14
 
15
15
  import astroid as _ast
16
16
 
17
+ from ._config import Config as _Config
17
18
  from ._directives import Comments as _Comments
18
19
  from ._directives import Directives as _Directives
19
20
  from ._stub import Docstring as _Docstring
@@ -22,15 +23,24 @@ from ._stub import Signature as _Signature
22
23
  from .messages import Messages as _Messages
23
24
 
24
25
 
26
+ class _Imports(_t.Dict[str, str]):
27
+ """Represents python imports."""
28
+
29
+
30
+ class _Overloads(_t.Dict[str, "Function"]):
31
+ """Represents overloaded methods."""
32
+
33
+
34
+ class _Children(_t.List[_t.Union["Parent", "Function"]]):
35
+ """Represents children of an object."""
36
+
37
+
25
38
  class Error(_Enum):
26
39
  """Represents an unrecoverable error."""
27
40
 
28
41
  SYNTAX = 1
29
42
  UNICODE = 2
30
-
31
-
32
- class _Imports(_t.Dict[str, str]):
33
- """Represents python imports."""
43
+ RECURSION = 3
34
44
 
35
45
 
36
46
  class Parent: # pylint: disable=too-many-instance-attributes
@@ -38,12 +48,8 @@ class Parent: # pylint: disable=too-many-instance-attributes
38
48
 
39
49
  :param node: AST node for this scope.
40
50
  :param directives: Directives and excluded errors per line.
41
- :param path: Path for this scope (or None).
42
- :param ignore_args: Ignore args prefixed with an asterisk.
43
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
44
- :param check_class_constructor: Check the class constructor's
45
- docstring. Otherwise, expect the constructor's documentation to
46
- be on the class level docstring.
51
+ :param file: Path for this scope (or None).
52
+ :param config: Configuration object.
47
53
  :param imports: Imports in this scope.
48
54
  :param error: Unrecoverable error for this scope, if any.
49
55
  """
@@ -59,28 +65,25 @@ class Parent: # pylint: disable=too-many-instance-attributes
59
65
  | None
60
66
  ) = None,
61
67
  directives: _Directives | None = None,
62
- path: _Path | None = None,
63
- ignore_args: bool = False,
64
- ignore_kwargs: bool = False,
65
- check_class_constructor: bool = False,
68
+ file: _Path | None = None,
69
+ config: _Config | None = None,
66
70
  imports: _Imports | None = None,
67
71
  error: Error | None = None,
68
72
  ) -> None:
69
73
  super().__init__()
70
- self._name = "module"
71
74
  self._error = error
72
- self._ignore_args = ignore_args
73
- self._ignore_kwargs = ignore_kwargs
74
- self._check_class_constructor = check_class_constructor
75
+ self._directives = directives or _Directives()
76
+ self._config = config or _Config()
75
77
  self._children = _Children()
76
78
  self._imports = imports or _Imports()
77
79
  self._overloads = _Overloads()
78
80
  if node is None:
81
+ self._name = "module"
79
82
  if not isinstance(self, Function) and error is not None:
80
- self._children.append(Function(path, error=error))
83
+ self._children.append(Function(file, error=error))
81
84
  else:
82
85
  self._name = node.name
83
- self._parse_ast(node, directives or _Directives(), path)
86
+ self._parse_ast(node, file)
84
87
 
85
88
  def _parse_ast(
86
89
  self,
@@ -90,19 +93,18 @@ class Parent: # pylint: disable=too-many-instance-attributes
90
93
  | _ast.nodes.FunctionDef
91
94
  | _ast.nodes.NodeNG
92
95
  ),
93
- directives: _Directives,
94
- path: _Path | None = None,
96
+ file: _Path | None = None,
95
97
  ) -> None:
96
98
  # need to keep track of `comments` as, even though they are
97
99
  # resolved in the directive object, they are needed to notify
98
100
  # the user in the case that they are invalid
99
- parent_comments, parent_disabled = directives.get(
101
+ parent_comments, parent_disabled = self._directives.get(
100
102
  node.lineno,
101
103
  (_Comments(), _Messages()),
102
104
  )
103
105
  if hasattr(node, "body"):
104
106
  for subnode in node.body:
105
- comments, disabled = directives.get(
107
+ comments, disabled = self._directives.get(
106
108
  subnode.lineno,
107
109
  (_Comments(), _Messages()),
108
110
  )
@@ -119,12 +121,10 @@ class Parent: # pylint: disable=too-many-instance-attributes
119
121
  func = Function(
120
122
  subnode,
121
123
  comments,
122
- directives,
124
+ self._directives,
123
125
  disabled,
124
- path,
125
- self._ignore_args,
126
- self._ignore_kwargs,
127
- self._check_class_constructor,
126
+ file,
127
+ self._config,
128
128
  self._imports,
129
129
  )
130
130
  if func.isoverloaded:
@@ -145,16 +145,14 @@ class Parent: # pylint: disable=too-many-instance-attributes
145
145
  self._children.append(
146
146
  Parent(
147
147
  subnode,
148
- directives,
149
- path,
150
- self._ignore_args,
151
- self._ignore_kwargs,
152
- self._check_class_constructor,
148
+ self._directives,
149
+ file,
150
+ self._config,
153
151
  self._imports,
154
152
  ),
155
153
  )
156
154
  else:
157
- self._parse_ast(subnode, directives, path)
155
+ self._parse_ast(subnode, file)
158
156
 
159
157
  @property
160
158
  def isprotected(self) -> bool:
@@ -179,12 +177,8 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
179
177
  :param comments: Comment directives for this function.
180
178
  :param directives: Directives keyed by line.
181
179
  :param messages: Disabled checks for this function.
182
- :param path: Path for this function (or None).
183
- :param ignore_args: Ignore args prefixed with an asterisk.
184
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
185
- :param check_class_constructor: If the function is the class
186
- constructor, use its own docstring. Otherwise, use the class
187
- level docstring for the constructor function.
180
+ :param file: Path for this function (or None).
181
+ :param config: Configuration object.
188
182
  :param imports: Imports in this scope.
189
183
  :param error: Unrecoverable error for this function, if any.
190
184
  """
@@ -196,20 +190,16 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
196
190
  comments: _Comments | None = None,
197
191
  directives: _Directives | None = None,
198
192
  messages: _Messages | None = None,
199
- path: _Path | None = None,
200
- ignore_args: bool = False,
201
- ignore_kwargs: bool = False,
202
- check_class_constructor: bool = False,
193
+ file: _Path | None = None,
194
+ config: _Config | None = None,
203
195
  imports: _Imports | None = None,
204
196
  error: Error | None = None,
205
197
  ) -> None:
206
198
  super().__init__(
207
199
  node,
208
200
  directives or _Directives(),
209
- path,
210
- ignore_args,
211
- ignore_kwargs,
212
- check_class_constructor,
201
+ file,
202
+ config,
213
203
  imports,
214
204
  )
215
205
  self._comments = comments or _Comments()
@@ -232,10 +222,9 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
232
222
 
233
223
  self._signature = self._signature.from_ast(
234
224
  node,
235
- ignore_args,
236
- ignore_kwargs,
225
+ self._config.ignore,
237
226
  )
238
- if self.isinit and not check_class_constructor:
227
+ if self.isinit and not self._config.check.class_constructor:
239
228
  # docstring for __init__ is expected on the class
240
229
  # docstring
241
230
  relevant_doc_node = self._parent.doc_node
@@ -364,11 +353,3 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
364
353
  :param rettype: Return type of the overloaded variant.
365
354
  """
366
355
  self._signature.overload(rettype)
367
-
368
-
369
- class _Overloads(_t.Dict[str, Function]):
370
- """Represents overloaded methods."""
371
-
372
-
373
- class _Children(_t.List[_t.Union[Parent, Function]]):
374
- """Represents children of an object."""
@@ -0,0 +1,89 @@
1
+ """
2
+ docsig._parsers
3
+ ===============
4
+ """
5
+
6
+ from __future__ import annotations as _
7
+
8
+ import logging as _logging
9
+ import os as _os
10
+ from pathlib import Path as _Path
11
+ from tokenize import TokenError as _TokenError
12
+
13
+ import astroid as _ast
14
+
15
+ from ._config import Config as _Config
16
+ from ._directives import Directives as _Directives
17
+ from ._files import FILE_INFO as _FILE_INFO
18
+ from ._module import Error as _Error
19
+ from ._module import Parent as _Parent
20
+
21
+
22
+ def parse_from_string(
23
+ code: str,
24
+ config: _Config,
25
+ module_name: str = "",
26
+ file: _Path | None = None,
27
+ ) -> _Parent:
28
+ """Build a Parent from a string of Python code.
29
+
30
+ Parses AST and comment directives (AST does not include comments,
31
+ so directives are parsed separately). On syntax error, returns a
32
+ Parent with an error set.
33
+
34
+ :param code: Python source to parse.
35
+ :param config: Configuration object.
36
+ :param module_name: Module name, or empty string.
37
+ :param file: Path for the source (or None for stdin).
38
+ :return: Parent for the parsed code or syntax error.
39
+ """
40
+ logger = _logging.getLogger(__package__)
41
+ source_name = file or "stdin"
42
+ try:
43
+ node = _ast.parse(code, module_name, str(file))
44
+ try:
45
+ directives = _Directives.from_text(code, config.disable)
46
+ except _TokenError as err:
47
+ directives = _Directives()
48
+ logger.debug(
49
+ _FILE_INFO,
50
+ source_name,
51
+ f"error parsing comments {err}".lower(),
52
+ )
53
+
54
+ parent = _Parent(node, directives, file, config)
55
+ logger.debug(_FILE_INFO, source_name, "Parsing Python code successful")
56
+ except _ast.AstroidSyntaxError as err:
57
+ logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
58
+ parent = _Parent(error=_Error.SYNTAX)
59
+ except RecursionError as err:
60
+ logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
61
+ parent = _Parent(error=_Error.RECURSION)
62
+
63
+ return parent
64
+
65
+
66
+ def parse_from_file(file: _Path, config: _Config) -> _Parent:
67
+ """Build a Parent from a file containing Python code.
68
+
69
+ Reads the file and delegates to parse_from_string. On UnicodeError,
70
+ returns a Parent with a Unicode error. On syntax error and a non-.py
71
+ path, returns an empty Parent (not treated as Python).
72
+
73
+ :param file: Path to the file to parse.
74
+ :param config: Configuration object.
75
+ :return: Parent for the parsed file or an error/empty Parent.
76
+ """
77
+ try:
78
+ code = file.read_text(encoding="utf-8")
79
+ module_name = str(file)[:-3].replace(_os.sep, ".").replace("-", "_")
80
+ parent = parse_from_string(code, config, module_name, file)
81
+ except UnicodeDecodeError as err:
82
+ logger = _logging.getLogger(__package__)
83
+ logger.debug(_FILE_INFO, file, str(err).replace("\n", " "))
84
+ parent = _Parent(error=_Error.UNICODE)
85
+
86
+ if parent.error is not None and not file.name.endswith(".py"):
87
+ parent = _Parent()
88
+
89
+ return parent
@@ -10,11 +10,14 @@ return the highest exit code.
10
10
 
11
11
  from __future__ import annotations as _
12
12
 
13
- import contextlib
13
+ import contextlib as _contextlib
14
+ import sys as _sys
14
15
  import typing as _t
16
+ from warnings import warn as _warn
15
17
 
16
18
  from astroid.nodes.scoped_nodes import scoped_nodes as _scoped_nodes
17
19
 
20
+ from ._config import Config as _Config
18
21
  from ._module import Error as _Error
19
22
  from ._module import Function as _Function
20
23
  from ._stub import UNNAMED as _UNNAMED
@@ -24,14 +27,19 @@ from ._stub import Params as _Params
24
27
  from ._stub import RetType as _RetType
25
28
  from ._utils import almost_equal as _almost_equal
26
29
  from ._utils import sentence_tokenizer as _sentence_tokenizer
30
+ from .messages import NEW as _NEW
31
+ from .messages import TEMPLATE as _TEMPLATE
27
32
  from .messages import E as _E
28
33
  from .messages import Message as _Message
29
- from .messages import Messages as _Messages
30
34
 
31
35
  _MIN_MATCH = 0.8
32
36
  _MAX_MATCH = 1.0
33
37
 
34
38
 
39
+ class Failures(_t.List["Failure"]):
40
+ """Sequence of Failure instances (one per function checked)."""
41
+
42
+
35
43
  class Failed(_t.NamedTuple):
36
44
  """Single reported issue."""
37
45
 
@@ -51,21 +59,17 @@ class Failure(_t.List[Failed]):
51
59
  violation.
52
60
 
53
61
  :param func: Function under check.
54
- :param target: List of errors to target.
55
- :param check_property_returns: Run return checks on properties.
62
+ :param config: Configuration object.
56
63
  """
57
64
 
58
- def __init__(
59
- self,
60
- func: _Function,
61
- target: _Messages,
62
- check_property_returns: bool,
63
- ) -> None:
65
+ def __init__(self, func: _Function, config: _Config) -> None:
64
66
  super().__init__()
65
67
  self._retcode = 0
66
68
  self._func = func
67
- if target:
68
- self._func.messages.extend(i for i in _E.all if i not in target)
69
+ if config.target:
70
+ self._func.messages.extend(
71
+ i for i in _E.all if i not in config.target
72
+ )
69
73
 
70
74
  self._name = self._func.name
71
75
  if (
@@ -89,7 +93,7 @@ class Failure(_t.List[Failed]):
89
93
  sig = self._func.signature.args.get(index)
90
94
  self._sig4xx_parameters(doc, sig)
91
95
 
92
- self._sig5xx_returns(check_property_returns)
96
+ self._sig5xx_returns(config.check.property_returns)
93
97
 
94
98
  self.sort()
95
99
 
@@ -139,7 +143,7 @@ class Failure(_t.List[Failed]):
139
143
  # then we know that they aren't almost equal, param1 is
140
144
  # missing and does need to be inserted in the docstring
141
145
  if is_equal:
142
- with contextlib.suppress(IndexError):
146
+ with _contextlib.suppress(IndexError):
143
147
  is_equal = to[count].name != from_[count + 1].name
144
148
 
145
149
  if not is_equal:
@@ -157,6 +161,13 @@ class Failure(_t.List[Failed]):
157
161
  else:
158
162
  # unknown-inline-directive
159
163
  self._add(_E[2], directive=comment.kind)
164
+ elif not comment.isvalidflag:
165
+ if comment.ismodule:
166
+ # unknown-module-directive-flag
167
+ self._add(_E[6], directive=comment.kind, flag=comment.flag)
168
+ else:
169
+ # unknown-inline-directive-flag
170
+ self._add(_E[7], directive=comment.kind, flag=comment.flag)
160
171
  else:
161
172
  for rule in comment:
162
173
  if not rule.isknown:
@@ -316,6 +327,8 @@ class Failure(_t.List[Failed]):
316
327
  self._retcode = 123
317
328
  if self._func.error == _Error.UNICODE:
318
329
  self._add(_E[902])
330
+ if self._func.error == _Error.RECURSION:
331
+ self._add(_E[903])
319
332
 
320
333
  @property
321
334
  def name(self) -> str:
@@ -333,5 +346,48 @@ class Failure(_t.List[Failed]):
333
346
  return self._retcode
334
347
 
335
348
 
336
- class Failures(_t.List[Failure]):
337
- """Sequence of Failure instances (one per function checked)."""
349
+ def report(
350
+ failures: Failures,
351
+ config: _Config,
352
+ file: str | None = None,
353
+ ) -> int:
354
+ """Print failures to stdout and return the highest exit code.
355
+
356
+ Iterates over failures, prints each with path, line header, and
357
+ messages, then returns the maximum retcode (0 or non-zero).
358
+
359
+ :param failures: Failures to print (one Failure per function).
360
+ :param config: Config for ANSI and formatting.
361
+ :param file: Module path when failures came from a file (optional).
362
+ :return: Exit code (non-zero if any check failed).
363
+ """
364
+ retcodes = [0]
365
+ for failure in failures:
366
+ retcodes.append(failure.retcode)
367
+ path_prefix = f"{file}:" if file is not None else ""
368
+ header = f"{path_prefix}{failure.lineno} in {failure.name}"
369
+ if not config.no_ansi and _sys.stdout.isatty():
370
+ header = f"\033[35m{header}\033[0m"
371
+
372
+ print(header)
373
+ for item in failure:
374
+ extra = None
375
+ if item.hint:
376
+ extra = f"hint: {item.hint}"
377
+
378
+ if item.new:
379
+ extra = "warning: please remember to fix this or disable it"
380
+ _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
381
+
382
+ print(
383
+ " "
384
+ + _TEMPLATE.format(
385
+ ref=item.ref,
386
+ description=item.description,
387
+ symbolic=item.symbolic,
388
+ ),
389
+ )
390
+ if extra is not None:
391
+ print(f" {extra}")
392
+
393
+ return max(retcodes)
@@ -16,6 +16,8 @@ from enum import Enum as _Enum
16
16
  import astroid as _ast
17
17
  import sphinx.ext.napoleon as _s
18
18
 
19
+ from ._config import Ignore as _Ignore
20
+
19
21
  # no function will accidentally have this name
20
22
  UNNAMED = "-1000"
21
23
 
@@ -100,11 +102,11 @@ class Param:
100
102
  indent: int = 0,
101
103
  closing_token: str = ":",
102
104
  ) -> None:
103
- self.kind = kind
104
- self.name = name
105
- self.description = description
106
- self.indent = indent
107
- self.closing_token = closing_token
105
+ self._kind = kind
106
+ self._name = name
107
+ self._description = description
108
+ self._indent = indent
109
+ self._closing_token = closing_token
108
110
 
109
111
  def __eq__(self, other: object) -> bool:
110
112
  iseq = False
@@ -125,33 +127,52 @@ class Param:
125
127
  """True if the parameter name starts with an underscore."""
126
128
  return str(self.name).startswith("_")
127
129
 
130
+ @property
131
+ def kind(self) -> DocType:
132
+ """Type of the param."""
133
+ return self._kind
134
+
135
+ @property
136
+ def name(self) -> str | None:
137
+ """Name of the param."""
138
+ return self._name
139
+
140
+ @property
141
+ def description(self) -> str | None:
142
+ """Description of param."""
143
+ return self._description
144
+
145
+ @property
146
+ def indent(self) -> int:
147
+ """Number of spaces in the indent."""
148
+ return self._indent
149
+
150
+ @property
151
+ def closing_token(self) -> str:
152
+ """Token used to terminate the param name definition."""
153
+ return self._closing_token
154
+
128
155
 
129
156
  class Params(_t.List[Param]):
130
157
  """A list-like collection of params.
131
158
 
132
- :param ignore_args: Ignore args prefixed with an asterisk.
133
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
159
+ :param ignore: Configuration object for what to ignore.
134
160
  """
135
161
 
136
- def __init__(
137
- self,
138
- ignore_args: bool = False,
139
- ignore_kwargs: bool = False,
140
- ) -> None:
162
+ def __init__(self, ignore: _Ignore) -> None:
141
163
  super().__init__()
142
- self._ignore_args = ignore_args
143
- self._ignore_kwargs = ignore_kwargs
164
+ self._ignore = ignore
144
165
  self._duplicates: list[Param] = []
145
166
 
146
167
  def append(self, value: Param) -> None:
147
168
  if not value.isprotected and any(
148
169
  (
149
170
  value.kind == DocType.PARAM,
150
- (value.kind == DocType.ARG and not self._ignore_args),
171
+ (value.kind == DocType.ARG and not self._ignore.args),
151
172
  (
152
173
  value.kind == DocType.KWARG
153
174
  and not (
154
- self._ignore_kwargs
175
+ self._ignore.kwargs
155
176
  or any(i.kind == DocType.KWARG for i in self)
156
177
  )
157
178
  ),
@@ -198,12 +219,8 @@ class Params(_t.List[Param]):
198
219
 
199
220
 
200
221
  class _Stub:
201
- def __init__(
202
- self,
203
- ignore_args: bool = False,
204
- ignore_kwargs: bool = False,
205
- ) -> None:
206
- self._args = Params(ignore_args, ignore_kwargs)
222
+ def __init__(self, ignore: _Ignore | None = None) -> None:
223
+ self._args = Params(ignore or _Ignore())
207
224
  self._returns = False
208
225
 
209
226
  @property
@@ -222,18 +239,16 @@ class Signature(_Stub):
222
239
 
223
240
  :param rettype: Kind of return (none, some, untyped).
224
241
  :param returns: True if return is declared in the signature.
225
- :param ignore_args: Ignore args prefixed with an asterisk.
226
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
242
+ :param ignore: Configuration object for what to ignore.
227
243
  """
228
244
 
229
245
  def __init__(
230
246
  self,
231
247
  rettype: RetType = RetType.NONE,
232
248
  returns: bool = False,
233
- ignore_args: bool = False,
234
- ignore_kwargs: bool = False,
249
+ ignore: _Ignore | None = None,
235
250
  ) -> None:
236
- super().__init__(ignore_args, ignore_kwargs)
251
+ super().__init__(ignore or _Ignore())
237
252
  self._rettype = rettype
238
253
  self._returns = returns
239
254
 
@@ -241,19 +256,17 @@ class Signature(_Stub):
241
256
  def from_ast(
242
257
  cls,
243
258
  node: _ast.nodes.Module | _ast.nodes.ClassDef | _ast.nodes.FunctionDef,
244
- ignore_args: bool = False,
245
- ignore_kwargs: bool = False,
259
+ ignore: _Ignore,
246
260
  ) -> Signature:
247
261
  """Build Signature from a function or class AST node.
248
262
 
249
263
  :param node: AST node (function, class, or module).
250
- :param ignore_args: Ignore args prefixed with an asterisk.
251
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
264
+ :param ignore: Configuration object for what to ignore.
252
265
  :return: Signature with args and return type.
253
266
  """
254
267
  rettype = RetType.from_ast(node.returns)
255
268
  returns = rettype == RetType.SOME
256
- signature = cls(rettype, returns, ignore_args, ignore_kwargs)
269
+ signature = cls(rettype, returns, ignore)
257
270
  # noinspection PyUnresolvedReferences
258
271
  for i in [
259
272
  a if isinstance(a, Param) else Param(name=a.name)
@@ -8,4 +8,4 @@ Allows for access to the version internally without cyclic imports
8
8
  caused by accessing it through __init__.
9
9
  """
10
10
 
11
- __version__ = "0.81.2"
11
+ __version__ = "0.82.0"
@@ -6,7 +6,7 @@ Error message definitions (Message, MessageMap, E) and format templates
6
6
  for docstring-check output.
7
7
  """
8
8
 
9
- from __future__ import annotations
9
+ from __future__ import annotations as _
10
10
 
11
11
  import typing as _t
12
12
 
@@ -24,6 +24,10 @@ NEW = """\
24
24
  """
25
25
 
26
26
 
27
+ class Messages(_t.List["Message"]):
28
+ """Sequence of Message instances, typically for one failure."""
29
+
30
+
27
31
  class Message(_t.NamedTuple):
28
32
  """One docstring-check error (ref, description, symbolic, hint)."""
29
33
 
@@ -63,10 +67,6 @@ class Message(_t.NamedTuple):
63
67
  )
64
68
 
65
69
 
66
- class Messages(_t.List[Message]):
67
- """Sequence of Message instances, typically for one failure."""
68
-
69
-
70
70
  class MessageMap(_t.Dict[int, Message]):
71
71
  """Mapping from integer key to Message (used for the E registry)."""
72
72
 
@@ -125,6 +125,16 @@ E = MessageMap(
125
125
  "both mutually exclusive class options configured",
126
126
  "mutually-exclusive-options",
127
127
  ),
128
+ 6: Message(
129
+ "SIG006",
130
+ "unknown module comment flag for {directive} '{flag}'",
131
+ "unknown-module-directive-flag",
132
+ ),
133
+ 7: Message(
134
+ "SIG007",
135
+ "unknown inline comment flag for {directive} '{flag}'",
136
+ "unknown-inline-directive-flag",
137
+ ),
128
138
  #: SIG1xx Missing
129
139
  101: Message(
130
140
  "SIG101",
@@ -252,5 +262,10 @@ E = MessageMap(
252
262
  "failed to read file",
253
263
  "unicode-decode-error",
254
264
  ),
265
+ 903: Message(
266
+ "SIG903",
267
+ "maximum recursion depth exceeded",
268
+ "recursion-error",
269
+ ),
255
270
  },
256
271
  )
@@ -11,7 +11,7 @@ line-length = 79
11
11
  allow_dirty = true
12
12
  commit = true
13
13
  commit_args = "-sS"
14
- current_version = "0.81.2"
14
+ current_version = "0.82.0"
15
15
  message = "bump: version {current_version} → {new_version}"
16
16
  sign_tags = true
17
17
  tag = true
@@ -122,7 +122,7 @@ maintainers = [
122
122
  name = "docsig"
123
123
  readme = "README.rst"
124
124
  repository = "https://github.com/jshwi/docsig"
125
- version = "0.81.2"
125
+ version = "0.82.0"
126
126
 
127
127
  [tool.poetry.dependencies]
128
128
  Sphinx = ">=7,<9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes