docsig 0.81.3__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.3
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.3, 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.3
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.3
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.3, 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.3
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.3
263
+ - docsig==0.82.0
264
264
  args:
265
265
  - "--sig-check-class"
266
266
  - "--sig-check-dunders"
@@ -13,23 +13,23 @@ from ._report import Failures as _Failures
13
13
 
14
14
 
15
15
  def _should_check_function(
16
- child: _Function,
16
+ function: _Function,
17
17
  parent: _Parent,
18
18
  config: _Config,
19
19
  ) -> bool:
20
- if child.isoverridden and not config.check.overridden:
20
+ if function.isoverridden and not config.check.overridden:
21
21
  return False
22
22
 
23
- if child.isprotected and not config.check.protected:
23
+ if function.isprotected and not config.check.protected:
24
24
  return False
25
25
 
26
- if child.isdunder and not config.check.dunders:
26
+ if function.isdunder and not config.check.dunders:
27
27
  return False
28
28
 
29
- if child.docstring.bare and config.ignore.no_params:
29
+ if function.docstring.bare and config.ignore.no_params:
30
30
  return False
31
31
 
32
- if child.isinit and (
32
+ if function.isinit and (
33
33
  not (config.check.class_ or config.check.class_constructor)
34
34
  or (parent.isprotected and not config.check.protected)
35
35
  ):
@@ -49,14 +49,14 @@ def _run_check(
49
49
  parent,
50
50
  config,
51
51
  ):
52
- failure = _Failure(child, config.target, config.check.property_returns)
52
+ failure = _Failure(child, config)
53
53
  if failure:
54
54
  failures.append(failure)
55
55
 
56
56
  # recurse for either class methods or, if enabled, nested functions
57
57
  if not isinstance(child, _Function) or config.check.nested:
58
- for func in child.children:
59
- _run_check(func, child, config, failures)
58
+ for child_of_child in child.children:
59
+ _run_check(child_of_child, child, config, failures)
60
60
 
61
61
 
62
62
  def run_checks(module: _Parent, config: _Config) -> _Failures:
@@ -71,12 +71,12 @@ def run_checks(module: _Parent, config: _Config) -> _Failures:
71
71
  :return: A list of function and class failures.
72
72
  """
73
73
  failures = _Failures()
74
- for top_level in module.children:
74
+ for child in module.children:
75
75
  if (
76
- not top_level.isprotected
76
+ not child.isprotected
77
77
  or config.check.protected
78
78
  or config.check.protected_class_methods
79
79
  ):
80
- _run_check(top_level, module, config, failures)
80
+ _run_check(child, module, config, failures)
81
81
 
82
82
  return failures
@@ -8,29 +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
21
16
  from ._check import run_checks as _run_checks
22
17
  from ._config import Check as _Check
23
18
  from ._config import Config as _Config
24
19
  from ._config import Ignore as _Ignore
25
- from ._directives import Directives as _Directives
26
- from ._files import FILE_INFO as _FILE_INFO
27
- from ._files import Paths as _Paths
28
- from ._module import Error as _Error
29
- from ._module import Parent as _Parent
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
30
23
  from ._report import Failures as _Failures
24
+ from ._report import report as _report
31
25
  from ._utils import print_checks as _print_checks
32
- from .messages import NEW as _NEW
33
- from .messages import TEMPLATE as _TEMPLATE
34
26
  from .messages import E as _E
35
27
  from .messages import Messages as _Messages
36
28
 
@@ -69,102 +61,6 @@ def setup_logger(verbose: bool) -> None:
69
61
  logger.addHandler(stream_handler)
70
62
 
71
63
 
72
- def _parse_from_file(path: _Path, config: _Config) -> _Parent:
73
- try:
74
- code = path.read_text(encoding="utf-8")
75
- parent = _parse_from_string(
76
- code,
77
- config,
78
- str(path)[:-3].replace(_os.sep, ".").replace("-", "_"),
79
- path,
80
- )
81
- except UnicodeDecodeError as err:
82
- logger = _logging.getLogger(__package__)
83
- logger.debug(_FILE_INFO, path, str(err).replace("\n", " "))
84
- parent = _Parent(error=_Error.UNICODE)
85
-
86
- if parent.error is not None and not path.name.endswith(".py"):
87
- parent = _Parent()
88
-
89
- return parent
90
-
91
-
92
- def _parse_from_string(
93
- code: str,
94
- config: _Config,
95
- module_name: str = "",
96
- path: _Path | None = None,
97
- ) -> _Parent:
98
- logger = _logging.getLogger(__package__)
99
- source_name = path or "stdin"
100
- try:
101
- node = _ast.parse(code, module_name, str(path))
102
- try:
103
- directives = _Directives.from_text(code, config.disable)
104
- except _TokenError as err:
105
- directives = _Directives()
106
- logger.debug(
107
- _FILE_INFO,
108
- source_name,
109
- f"error parsing comments {err}".lower(),
110
- )
111
-
112
- parent = _Parent(
113
- node,
114
- directives,
115
- path,
116
- config.ignore.args,
117
- config.ignore.kwargs,
118
- config.check.class_constructor,
119
- )
120
- logger.debug(_FILE_INFO, source_name, "Parsing Python code successful")
121
- except _ast.AstroidSyntaxError as err:
122
- logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
123
- parent = _Parent(error=_Error.SYNTAX)
124
- except RecursionError as err:
125
- logger.debug(_FILE_INFO, source_name, str(err).replace("\n", " "))
126
- parent = _Parent(error=_Error.RECURSION)
127
-
128
- return parent
129
-
130
-
131
- def _report(
132
- failures: _Failures,
133
- config: _Config,
134
- path: str | None = None,
135
- ) -> int:
136
- retcodes = [0]
137
- for failure in failures:
138
- retcodes.append(failure.retcode)
139
- path_prefix = f"{path}:" if path is not None else ""
140
- header = f"{path_prefix}{failure.lineno} in {failure.name}"
141
- if not config.no_ansi and _sys.stdout.isatty():
142
- header = f"\033[35m{header}\033[0m"
143
-
144
- print(header)
145
- for item in failure:
146
- extra = None
147
- if item.hint:
148
- extra = f"hint: {item.hint}"
149
-
150
- if item.new:
151
- extra = "warning: please remember to fix this or disable it"
152
- _warn(_NEW.format(ref=item.ref), FutureWarning, stacklevel=3)
153
-
154
- print(
155
- " "
156
- + _TEMPLATE.format(
157
- ref=item.ref,
158
- description=item.description,
159
- symbolic=item.symbolic,
160
- ),
161
- )
162
- if extra is not None:
163
- print(f" {extra}")
164
-
165
- return max(retcodes)
166
-
167
-
168
64
  def handle_deprecations(
169
65
  ignore_typechecker: bool,
170
66
  disable: list,
@@ -187,14 +83,14 @@ def handle_deprecations(
187
83
  disable.extend(messages)
188
84
 
189
85
 
190
- def runner(path: _Path, config: _Config) -> _Failures:
86
+ def runner(file: _Path, config: _Config) -> _Failures:
191
87
  """Run checks for a single file and return collected failures.
192
88
 
193
- :param path: Path to the file to check.
89
+ :param file: Path to the file to check.
194
90
  :param config: Configuration object.
195
91
  :return: Collected failures for the file.
196
92
  """
197
- module = _parse_from_file(path, config)
93
+ module = _parse_from_file(file, config)
198
94
  return _run_checks(module, config)
199
95
 
200
96
 
@@ -315,14 +211,10 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments
315
211
  return _report(failures, config)
316
212
 
317
213
  retcodes = [0]
318
- paths = _Paths(
319
- *path,
320
- patterns=config.exclude,
321
- excludes=config.excludes,
322
- include_ignored=config.include_ignored,
323
- )
324
- for path_ in paths:
325
- failures = runner(path_, config)
326
- retcodes.append(_report(failures, config, str(path_)))
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)
327
219
 
328
220
  return max(retcodes)
@@ -27,12 +27,16 @@ class Comment(_Messages):
27
27
  """
28
28
 
29
29
  _valid_kinds = "enable", "disable"
30
+ _valid_flags = ("next",)
30
31
 
31
32
  def __init__(self, string: str, col: int) -> None:
32
33
  super().__init__()
33
34
  self._ismodule = col == 0
34
35
  parts = string.split("=")
35
- 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"
36
40
  if len(parts) == 1:
37
41
  self.extend(_E.all)
38
42
  else:
@@ -63,6 +67,21 @@ class Comment(_Messages):
63
67
  """Whether this is a disable directive."""
64
68
  return self._kind == self._valid_kinds[1]
65
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
+
66
85
  @classmethod
67
86
  def parse(cls, comment: str, col: int) -> Comment | None:
68
87
  """Parse a comment into a docsig directive when present.
@@ -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
@@ -47,12 +48,8 @@ class Parent: # pylint: disable=too-many-instance-attributes
47
48
 
48
49
  :param node: AST node for this scope.
49
50
  :param directives: Directives and excluded errors per line.
50
- :param path: Path for this scope (or None).
51
- :param ignore_args: Ignore args prefixed with an asterisk.
52
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
53
- :param check_class_constructor: Check the class constructor's
54
- docstring. Otherwise, expect the constructor's documentation to
55
- be on the class level docstring.
51
+ :param file: Path for this scope (or None).
52
+ :param config: Configuration object.
56
53
  :param imports: Imports in this scope.
57
54
  :param error: Unrecoverable error for this scope, if any.
58
55
  """
@@ -68,29 +65,25 @@ class Parent: # pylint: disable=too-many-instance-attributes
68
65
  | None
69
66
  ) = None,
70
67
  directives: _Directives | None = None,
71
- path: _Path | None = None,
72
- ignore_args: bool = False,
73
- ignore_kwargs: bool = False,
74
- check_class_constructor: bool = False,
68
+ file: _Path | None = None,
69
+ config: _Config | None = None,
75
70
  imports: _Imports | None = None,
76
71
  error: Error | None = None,
77
72
  ) -> None:
78
73
  super().__init__()
79
74
  self._error = error
80
75
  self._directives = directives or _Directives()
81
- self._ignore_args = ignore_args
82
- self._ignore_kwargs = ignore_kwargs
83
- self._check_class_constructor = check_class_constructor
76
+ self._config = config or _Config()
84
77
  self._children = _Children()
85
78
  self._imports = imports or _Imports()
86
79
  self._overloads = _Overloads()
87
80
  if node is None:
88
81
  self._name = "module"
89
82
  if not isinstance(self, Function) and error is not None:
90
- self._children.append(Function(path, error=error))
83
+ self._children.append(Function(file, error=error))
91
84
  else:
92
85
  self._name = node.name
93
- self._parse_ast(node, path)
86
+ self._parse_ast(node, file)
94
87
 
95
88
  def _parse_ast(
96
89
  self,
@@ -100,7 +93,7 @@ class Parent: # pylint: disable=too-many-instance-attributes
100
93
  | _ast.nodes.FunctionDef
101
94
  | _ast.nodes.NodeNG
102
95
  ),
103
- path: _Path | None = None,
96
+ file: _Path | None = None,
104
97
  ) -> None:
105
98
  # need to keep track of `comments` as, even though they are
106
99
  # resolved in the directive object, they are needed to notify
@@ -130,10 +123,8 @@ class Parent: # pylint: disable=too-many-instance-attributes
130
123
  comments,
131
124
  self._directives,
132
125
  disabled,
133
- path,
134
- self._ignore_args,
135
- self._ignore_kwargs,
136
- self._check_class_constructor,
126
+ file,
127
+ self._config,
137
128
  self._imports,
138
129
  )
139
130
  if func.isoverloaded:
@@ -155,15 +146,13 @@ class Parent: # pylint: disable=too-many-instance-attributes
155
146
  Parent(
156
147
  subnode,
157
148
  self._directives,
158
- path,
159
- self._ignore_args,
160
- self._ignore_kwargs,
161
- self._check_class_constructor,
149
+ file,
150
+ self._config,
162
151
  self._imports,
163
152
  ),
164
153
  )
165
154
  else:
166
- self._parse_ast(subnode, path)
155
+ self._parse_ast(subnode, file)
167
156
 
168
157
  @property
169
158
  def isprotected(self) -> bool:
@@ -188,12 +177,8 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
188
177
  :param comments: Comment directives for this function.
189
178
  :param directives: Directives keyed by line.
190
179
  :param messages: Disabled checks for this function.
191
- :param path: Path for this function (or None).
192
- :param ignore_args: Ignore args prefixed with an asterisk.
193
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
194
- :param check_class_constructor: If the function is the class
195
- constructor, use its own docstring. Otherwise, use the class
196
- level docstring for the constructor function.
180
+ :param file: Path for this function (or None).
181
+ :param config: Configuration object.
197
182
  :param imports: Imports in this scope.
198
183
  :param error: Unrecoverable error for this function, if any.
199
184
  """
@@ -205,20 +190,16 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
205
190
  comments: _Comments | None = None,
206
191
  directives: _Directives | None = None,
207
192
  messages: _Messages | None = None,
208
- path: _Path | None = None,
209
- ignore_args: bool = False,
210
- ignore_kwargs: bool = False,
211
- check_class_constructor: bool = False,
193
+ file: _Path | None = None,
194
+ config: _Config | None = None,
212
195
  imports: _Imports | None = None,
213
196
  error: Error | None = None,
214
197
  ) -> None:
215
198
  super().__init__(
216
199
  node,
217
200
  directives or _Directives(),
218
- path,
219
- ignore_args,
220
- ignore_kwargs,
221
- check_class_constructor,
201
+ file,
202
+ config,
222
203
  imports,
223
204
  )
224
205
  self._comments = comments or _Comments()
@@ -241,10 +222,9 @@ class Function(Parent): # pylint: disable=too-many-instance-attributes
241
222
 
242
223
  self._signature = self._signature.from_ast(
243
224
  node,
244
- ignore_args,
245
- ignore_kwargs,
225
+ self._config.ignore,
246
226
  )
247
- if self.isinit and not check_class_constructor:
227
+ if self.isinit and not self._config.check.class_constructor:
248
228
  # docstring for __init__ is expected on the class
249
229
  # docstring
250
230
  relevant_doc_node = self._parent.doc_node
@@ -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
@@ -11,10 +11,13 @@ return the highest exit code.
11
11
  from __future__ import annotations as _
12
12
 
13
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,9 +27,10 @@ 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
@@ -55,21 +59,17 @@ class Failure(_t.List[Failed]):
55
59
  violation.
56
60
 
57
61
  :param func: Function under check.
58
- :param target: List of errors to target.
59
- :param check_property_returns: Run return checks on properties.
62
+ :param config: Configuration object.
60
63
  """
61
64
 
62
- def __init__(
63
- self,
64
- func: _Function,
65
- target: _Messages,
66
- check_property_returns: bool,
67
- ) -> None:
65
+ def __init__(self, func: _Function, config: _Config) -> None:
68
66
  super().__init__()
69
67
  self._retcode = 0
70
68
  self._func = func
71
- if target:
72
- 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
+ )
73
73
 
74
74
  self._name = self._func.name
75
75
  if (
@@ -93,7 +93,7 @@ class Failure(_t.List[Failed]):
93
93
  sig = self._func.signature.args.get(index)
94
94
  self._sig4xx_parameters(doc, sig)
95
95
 
96
- self._sig5xx_returns(check_property_returns)
96
+ self._sig5xx_returns(config.check.property_returns)
97
97
 
98
98
  self.sort()
99
99
 
@@ -161,6 +161,13 @@ class Failure(_t.List[Failed]):
161
161
  else:
162
162
  # unknown-inline-directive
163
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)
164
171
  else:
165
172
  for rule in comment:
166
173
  if not rule.isknown:
@@ -337,3 +344,50 @@ class Failure(_t.List[Failed]):
337
344
  def retcode(self) -> int:
338
345
  """Exit code (non-zero if any check failed)."""
339
346
  return self._retcode
347
+
348
+
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
 
@@ -154,29 +156,23 @@ class Param:
154
156
  class Params(_t.List[Param]):
155
157
  """A list-like collection of params.
156
158
 
157
- :param ignore_args: Ignore args prefixed with an asterisk.
158
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
159
+ :param ignore: Configuration object for what to ignore.
159
160
  """
160
161
 
161
- def __init__(
162
- self,
163
- ignore_args: bool = False,
164
- ignore_kwargs: bool = False,
165
- ) -> None:
162
+ def __init__(self, ignore: _Ignore) -> None:
166
163
  super().__init__()
167
- self._ignore_args = ignore_args
168
- self._ignore_kwargs = ignore_kwargs
164
+ self._ignore = ignore
169
165
  self._duplicates: list[Param] = []
170
166
 
171
167
  def append(self, value: Param) -> None:
172
168
  if not value.isprotected and any(
173
169
  (
174
170
  value.kind == DocType.PARAM,
175
- (value.kind == DocType.ARG and not self._ignore_args),
171
+ (value.kind == DocType.ARG and not self._ignore.args),
176
172
  (
177
173
  value.kind == DocType.KWARG
178
174
  and not (
179
- self._ignore_kwargs
175
+ self._ignore.kwargs
180
176
  or any(i.kind == DocType.KWARG for i in self)
181
177
  )
182
178
  ),
@@ -223,12 +219,8 @@ class Params(_t.List[Param]):
223
219
 
224
220
 
225
221
  class _Stub:
226
- def __init__(
227
- self,
228
- ignore_args: bool = False,
229
- ignore_kwargs: bool = False,
230
- ) -> None:
231
- self._args = Params(ignore_args, ignore_kwargs)
222
+ def __init__(self, ignore: _Ignore | None = None) -> None:
223
+ self._args = Params(ignore or _Ignore())
232
224
  self._returns = False
233
225
 
234
226
  @property
@@ -247,18 +239,16 @@ class Signature(_Stub):
247
239
 
248
240
  :param rettype: Kind of return (none, some, untyped).
249
241
  :param returns: True if return is declared in the signature.
250
- :param ignore_args: Ignore args prefixed with an asterisk.
251
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
242
+ :param ignore: Configuration object for what to ignore.
252
243
  """
253
244
 
254
245
  def __init__(
255
246
  self,
256
247
  rettype: RetType = RetType.NONE,
257
248
  returns: bool = False,
258
- ignore_args: bool = False,
259
- ignore_kwargs: bool = False,
249
+ ignore: _Ignore | None = None,
260
250
  ) -> None:
261
- super().__init__(ignore_args, ignore_kwargs)
251
+ super().__init__(ignore or _Ignore())
262
252
  self._rettype = rettype
263
253
  self._returns = returns
264
254
 
@@ -266,19 +256,17 @@ class Signature(_Stub):
266
256
  def from_ast(
267
257
  cls,
268
258
  node: _ast.nodes.Module | _ast.nodes.ClassDef | _ast.nodes.FunctionDef,
269
- ignore_args: bool = False,
270
- ignore_kwargs: bool = False,
259
+ ignore: _Ignore,
271
260
  ) -> Signature:
272
261
  """Build Signature from a function or class AST node.
273
262
 
274
263
  :param node: AST node (function, class, or module).
275
- :param ignore_args: Ignore args prefixed with an asterisk.
276
- :param ignore_kwargs: Ignore kwargs prefixed with two asterisks.
264
+ :param ignore: Configuration object for what to ignore.
277
265
  :return: Signature with args and return type.
278
266
  """
279
267
  rettype = RetType.from_ast(node.returns)
280
268
  returns = rettype == RetType.SOME
281
- signature = cls(rettype, returns, ignore_args, ignore_kwargs)
269
+ signature = cls(rettype, returns, ignore)
282
270
  # noinspection PyUnresolvedReferences
283
271
  for i in [
284
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.3"
11
+ __version__ = "0.82.0"
@@ -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",
@@ -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.3"
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.3"
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
File without changes