check-dependencies 0.9.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Micha Schoell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.1
2
+ Name: check-dependencies
3
+ Version: 0.9.0
4
+ Summary:
5
+ Author: Micha Scholl
6
+ Author-email: schollm-git@gmx.com
7
+ Requires-Python: >=3.8
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: toml
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Check Dependencies
18
+ Check all imports from python files and compares them against the declared imports of a pyproject dependency list of expected imports.
19
+ It can be used as a stand-alone or as part of a CI/CD to check if an application has all the necessary, but no superfluous imports.
20
+
21
+ ## Usage
22
+ ```commandline
23
+ usage: check_imports [-h] [--config-file CONFIG_FILE] [--include-dev] [--verbose] [--all] [--extra EXTRA] [--ignore IGNORE] file_name [file_name ...]
24
+
25
+ Find undeclared and unused (or all) imports in Python files
26
+
27
+ positional arguments:
28
+ file_name Python Source file to analyse
29
+
30
+ optional arguments:
31
+ -h, --help show this help message and exit
32
+ --config-file CONFIG_FILE
33
+ Location of pyproject.toml file, can be file or a directory containing pyproject.toml file
34
+ --include-dev Include dev dependencies
35
+ --verbose Show every import of a package
36
+ --all Show all imports (including correct ones)
37
+ --extra EXTRA Comma seperated list of extra requirements. Assume they are part of the requirements
38
+ --ignore IGNORE Comma seperated list of requirements to ignore. Assume they are not part of the requirements
39
+ ```
40
+
41
+ ```commandline
42
+ > python -m --config-file --all project/ project/src/**/*.py
43
+ pandas
44
+ ! matplotlib
45
+ numpy
46
+ + requests
47
+
48
+ > python -m check_dependencies --config-file --verbose project/ project/src/**/*.py
49
+ !NA matplotlib project/src/main.py:4
50
+ +EXTRA project/pyproject.toml requests
51
+
52
+ > python -m check_dependencies --config-file --verbose --all project/ project/src/**/*.py
53
+ OK project/src/data.py:5 pandas
54
+ OK project/src/main.py:3 pandas
55
+ OK project/src/plotting.py:4 pandas
56
+ !NA project/src/plotting.py:5 matplotlib
57
+ OK project/src/plotting.py:6 numpy
58
+
59
+ **** Dependencies in config file not used in application:
60
+ +EXTRA project/pyproject.toml requests
61
+
62
+ ```
63
+
64
+ ### Configuration
65
+ The configuration is read from `pyproject.toml` file. The configuration file
66
+ supports two entries, `[tool.check_dependencies.extra-requirements]` that can be used to
67
+ add extra dependencies to the list of requirements to be treated as existing
68
+ requirements.
69
+ The second entry, `[tool.check_dependencies.ignore-requirements]` does the opposite, it will
70
+ ignore extra requirements that are not used in the application.
71
+
72
+ ```toml
73
+ [tool.check_dependencies]
74
+ extra-requirements = [
75
+ undeclared_package,
76
+ another_package
77
+ ]
78
+ ignore-requirements = [
79
+ package_as_extra_for_another_package,
80
+ yet_another_package
81
+ ]
82
+ ```
83
+
84
+ ### Output
85
+ The output is a list of imports with a prefix indicating the status of the import.
86
+ - `!` - Undeclared import
87
+ - `+` - Extra import, declared in pyproject.toml, but not used in the file
88
+ - ` ` - Correct import (only shown with `--all`)
89
+
90
+ In case of `--verbose`, the output is a list of all imports in the file, prefixed with:
91
+ - `!NA` - Undeclared import
92
+ - `+EXTRA` - Extra import, declared in pyproject.toml, but not used in the file
93
+ - ` OK` - Correct import (only shown with `--all`)
94
+
95
+ In case of `--verbose`, each import is prefixed with the file name and line number
96
+ where it is declared.
97
+
98
+ #### Exit code
99
+ - 0: No missing or superfluous dependencies found
100
+ - 2: Missing (used, but not declared in pyproject.toml) dependencies found
101
+ - 4: Extra (declared in pyproject.toml, but unused) dependencies found
102
+ - 6: Both missing and superfluous dependencies found
103
+ - 1: Another error occured
104
+
105
+
106
+ ## Development
107
+ Feature requests and merge requests are welcome. For major changes, please open an
108
+ issue first to discuss what you would like to change.
109
+
110
+ Please make sure to update tests as appropriate. Also with this project, I want
111
+ to keep the dependencies to a minimum, so please keep that in mind when proposing
112
+ a change. Currently, the only dependencies is `toml` to support Python 3.10 and below.
113
+
114
+ ### Coding Standards
115
+
116
+ | **Type** | Package | Comment |
117
+ |---------------|----------|---------------------------------|
118
+ | **Linter** | `black` | Also for auto-formatted modules |
119
+ | **Logging** | `logger` | Minimize additional packages |
120
+ | **Packaging** | `poetry` | |
121
+ | **Tests** | `pytest` | |
122
+ | **Typing** | `mypy` | Type all methods |
123
+ | **Linting** | `flake8` | |
124
+ | **Imports** | `isort` | |
125
+
@@ -0,0 +1,108 @@
1
+ # Check Dependencies
2
+ Check all imports from python files and compares them against the declared imports of a pyproject dependency list of expected imports.
3
+ It can be used as a stand-alone or as part of a CI/CD to check if an application has all the necessary, but no superfluous imports.
4
+
5
+ ## Usage
6
+ ```commandline
7
+ usage: check_imports [-h] [--config-file CONFIG_FILE] [--include-dev] [--verbose] [--all] [--extra EXTRA] [--ignore IGNORE] file_name [file_name ...]
8
+
9
+ Find undeclared and unused (or all) imports in Python files
10
+
11
+ positional arguments:
12
+ file_name Python Source file to analyse
13
+
14
+ optional arguments:
15
+ -h, --help show this help message and exit
16
+ --config-file CONFIG_FILE
17
+ Location of pyproject.toml file, can be file or a directory containing pyproject.toml file
18
+ --include-dev Include dev dependencies
19
+ --verbose Show every import of a package
20
+ --all Show all imports (including correct ones)
21
+ --extra EXTRA Comma seperated list of extra requirements. Assume they are part of the requirements
22
+ --ignore IGNORE Comma seperated list of requirements to ignore. Assume they are not part of the requirements
23
+ ```
24
+
25
+ ```commandline
26
+ > python -m --config-file --all project/ project/src/**/*.py
27
+ pandas
28
+ ! matplotlib
29
+ numpy
30
+ + requests
31
+
32
+ > python -m check_dependencies --config-file --verbose project/ project/src/**/*.py
33
+ !NA matplotlib project/src/main.py:4
34
+ +EXTRA project/pyproject.toml requests
35
+
36
+ > python -m check_dependencies --config-file --verbose --all project/ project/src/**/*.py
37
+ OK project/src/data.py:5 pandas
38
+ OK project/src/main.py:3 pandas
39
+ OK project/src/plotting.py:4 pandas
40
+ !NA project/src/plotting.py:5 matplotlib
41
+ OK project/src/plotting.py:6 numpy
42
+
43
+ **** Dependencies in config file not used in application:
44
+ +EXTRA project/pyproject.toml requests
45
+
46
+ ```
47
+
48
+ ### Configuration
49
+ The configuration is read from `pyproject.toml` file. The configuration file
50
+ supports two entries, `[tool.check_dependencies.extra-requirements]` that can be used to
51
+ add extra dependencies to the list of requirements to be treated as existing
52
+ requirements.
53
+ The second entry, `[tool.check_dependencies.ignore-requirements]` does the opposite, it will
54
+ ignore extra requirements that are not used in the application.
55
+
56
+ ```toml
57
+ [tool.check_dependencies]
58
+ extra-requirements = [
59
+ undeclared_package,
60
+ another_package
61
+ ]
62
+ ignore-requirements = [
63
+ package_as_extra_for_another_package,
64
+ yet_another_package
65
+ ]
66
+ ```
67
+
68
+ ### Output
69
+ The output is a list of imports with a prefix indicating the status of the import.
70
+ - `!` - Undeclared import
71
+ - `+` - Extra import, declared in pyproject.toml, but not used in the file
72
+ - ` ` - Correct import (only shown with `--all`)
73
+
74
+ In case of `--verbose`, the output is a list of all imports in the file, prefixed with:
75
+ - `!NA` - Undeclared import
76
+ - `+EXTRA` - Extra import, declared in pyproject.toml, but not used in the file
77
+ - ` OK` - Correct import (only shown with `--all`)
78
+
79
+ In case of `--verbose`, each import is prefixed with the file name and line number
80
+ where it is declared.
81
+
82
+ #### Exit code
83
+ - 0: No missing or superfluous dependencies found
84
+ - 2: Missing (used, but not declared in pyproject.toml) dependencies found
85
+ - 4: Extra (declared in pyproject.toml, but unused) dependencies found
86
+ - 6: Both missing and superfluous dependencies found
87
+ - 1: Another error occured
88
+
89
+
90
+ ## Development
91
+ Feature requests and merge requests are welcome. For major changes, please open an
92
+ issue first to discuss what you would like to change.
93
+
94
+ Please make sure to update tests as appropriate. Also with this project, I want
95
+ to keep the dependencies to a minimum, so please keep that in mind when proposing
96
+ a change. Currently, the only dependencies is `toml` to support Python 3.10 and below.
97
+
98
+ ### Coding Standards
99
+
100
+ | **Type** | Package | Comment |
101
+ |---------------|----------|---------------------------------|
102
+ | **Linter** | `black` | Also for auto-formatted modules |
103
+ | **Logging** | `logger` | Minimize additional packages |
104
+ | **Packaging** | `poetry` | |
105
+ | **Tests** | `pytest` | |
106
+ | **Typing** | `mypy` | Type all methods |
107
+ | **Linting** | `flake8` | |
108
+ | **Imports** | `isort` | |
@@ -0,0 +1,33 @@
1
+ [tool.poetry]
2
+ name = "check-dependencies"
3
+ version = "0.9.0"
4
+ description = ""
5
+ authors = ["Micha Scholl <schollm-git@gmx.com>"]
6
+ readme = "README.md"
7
+
8
+ [tool.poetry.dependencies]
9
+ python = ">=3.8"
10
+ toml = "*"
11
+
12
+ [tool.poetry.group.dev.dependencies]
13
+ pytest = "^6.2"
14
+ mypy = "^1.10.0"
15
+ black = "^24.4.2"
16
+ isort = "^5.13.2"
17
+ pylint = "^3.2.3"
18
+ pytest-cov = "^5.0.0"
19
+
20
+ [build-system]
21
+ requires = ["poetry-core"]
22
+ build-backend = "poetry.core.masonry.api"
23
+
24
+ [tool.black]
25
+ line-length = 88
26
+ target-version = ['py39']
27
+
28
+ [tool.isort]
29
+ sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER']
30
+ multi_line_output = 3
31
+ line_length = 88
32
+ include_trailing_comma = 'True'
33
+ profile = "black"
@@ -0,0 +1,84 @@
1
+ """CLI entry point for check_dependencies."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+
7
+ from check_dependencies.lib import Config
8
+ from check_dependencies.main import yield_wrong_imports
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format="%(filename)s: "
13
+ "%(levelname)-8s: "
14
+ "%(funcName)s(): "
15
+ "%(lineno)d:\t"
16
+ "%(message)s",
17
+ )
18
+
19
+ parser = argparse.ArgumentParser(
20
+ "check_dependencies",
21
+ description="Find undeclared and unused (or all) imports in Python files",
22
+ add_help=True,
23
+ )
24
+
25
+ parser.add_argument(
26
+ "file_name", type=str, nargs="+", help="Python Source file to analyse"
27
+ )
28
+ parser.add_argument(
29
+ "--config-file",
30
+ type=str,
31
+ required=False,
32
+ default="",
33
+ help="Location of pyproject.toml file, can be file or a directory"
34
+ " containing pyproject.toml file",
35
+ )
36
+ parser.add_argument(
37
+ "--include-dev",
38
+ action="store_true",
39
+ default=False,
40
+ help="Include dev dependencies",
41
+ )
42
+ parser.add_argument(
43
+ "--verbose",
44
+ action="store_true",
45
+ default=False,
46
+ help="Show every import of a package",
47
+ )
48
+ parser.add_argument(
49
+ "--all",
50
+ action="store_true",
51
+ default=False,
52
+ help="Show all imports (including correct ones)",
53
+ )
54
+ parser.add_argument(
55
+ "--extra",
56
+ type=str,
57
+ help="Comma seperated list of extra requirements."
58
+ " Assume they are part of the requirements",
59
+ default="",
60
+ )
61
+ parser.add_argument(
62
+ "--ignore",
63
+ type=str,
64
+ help="Comma seperated list of requirements to ignore."
65
+ " Assume they are not part of the requirements",
66
+ default="",
67
+ )
68
+ args = parser.parse_args()
69
+
70
+ cfg = Config(
71
+ file=args.config_file,
72
+ include_dev=args.include_dev,
73
+ verbose=args.verbose,
74
+ show_all=args.all,
75
+ extra_requirements=args.extra.split(","),
76
+ ignore_requirements=args.ignore.split(","),
77
+ )
78
+
79
+ wrong_import_lines = yield_wrong_imports(args.file_name, cfg)
80
+ try:
81
+ while True:
82
+ print(next(wrong_import_lines))
83
+ except StopIteration as ex: # Return value is the exit status
84
+ sys.exit(ex.value)
@@ -0,0 +1,329 @@
1
+ """A list of all builtin modules"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ BUILTINS: set[str]
8
+ if sys.version_info >= (3, 10):
9
+ BUILTINS = set(sys.stdlib_module_names) # pylint: disable=no-member
10
+ else:
11
+ # Use the list of builtins from Python 3.9
12
+ BUILTINS = set(
13
+ """\
14
+ __future__
15
+ _abc
16
+ _aix_support
17
+ _ast
18
+ _asyncio
19
+ _bisect
20
+ _blake2
21
+ _bootlocale
22
+ _bootsubprocess
23
+ _bz2
24
+ _codecs
25
+ _codecs_cn
26
+ _codecs_hk
27
+ _codecs_iso2022
28
+ _codecs_jp
29
+ _codecs_kr
30
+ _codecs_tw
31
+ _collections
32
+ _collections_abc
33
+ _compat_pickle
34
+ _compression
35
+ _contextvars
36
+ _crypt
37
+ _csv
38
+ _ctypes
39
+ _ctypes_test
40
+ _curses
41
+ _curses_panel
42
+ _datetime
43
+ _dbm
44
+ _decimal
45
+ _distutils_hack
46
+ _elementtree
47
+ _functools
48
+ _gdbm
49
+ _hashlib
50
+ _heapq
51
+ _imp
52
+ _io
53
+ _json
54
+ _locale
55
+ _lsprof
56
+ _lzma
57
+ _markupbase
58
+ _md5
59
+ _multibytecodec
60
+ _multiprocessing
61
+ _opcode
62
+ _operator
63
+ _osx_support
64
+ _peg_parser
65
+ _pickle
66
+ _posixshmem
67
+ _posixsubprocess
68
+ _py_abc
69
+ _pydecimal
70
+ _pyio
71
+ _queue
72
+ _random
73
+ _scproxy
74
+ _sha1
75
+ _sha256
76
+ _sha3
77
+ _sha512
78
+ _signal
79
+ _sitebuiltins
80
+ _socket
81
+ _sqlite3
82
+ _sre
83
+ _ssl
84
+ _stat
85
+ _statistics
86
+ _string
87
+ _strptime
88
+ _struct
89
+ _symtable
90
+ _sysconfigdata__darwin_darwin
91
+ _testbuffer
92
+ _testcapi
93
+ _testimportmultiple
94
+ _testinternalcapi
95
+ _testmultiphase
96
+ _thread
97
+ _threading_local
98
+ _tkinter
99
+ _tracemalloc
100
+ _uuid
101
+ _virtualenv
102
+ _warnings
103
+ _weakref
104
+ _weakrefset
105
+ _xxsubinterpreters
106
+ _xxtestfuzz
107
+ _zoneinfo
108
+ abc
109
+ aifc
110
+ antigravity
111
+ argparse
112
+ array
113
+ ast
114
+ asynchat
115
+ asyncio
116
+ asyncore
117
+ atexit
118
+ audioop
119
+ base64
120
+ bdb
121
+ binascii
122
+ binhex
123
+ bisect
124
+ builtins
125
+ bz2
126
+ cProfile
127
+ calendar
128
+ cgi
129
+ cgitb
130
+ chunk
131
+ cmath
132
+ cmd
133
+ code
134
+ codecs
135
+ codeop
136
+ collections
137
+ colorsys
138
+ compileall
139
+ concurrent
140
+ configparser
141
+ contextlib
142
+ contextvars
143
+ copy
144
+ copyreg
145
+ crypt
146
+ csv
147
+ ctypes
148
+ curses
149
+ dataclasses
150
+ datetime
151
+ dbm
152
+ decimal
153
+ difflib
154
+ dis
155
+ distutils
156
+ doctest
157
+ email
158
+ encodings
159
+ ensurepip
160
+ enum
161
+ errno
162
+ faulthandler
163
+ fcntl
164
+ filecmp
165
+ fileinput
166
+ fnmatch
167
+ formatter
168
+ fractions
169
+ ftplib
170
+ functools
171
+ gc
172
+ genericpath
173
+ getopt
174
+ getpass
175
+ gettext
176
+ glob
177
+ graphlib
178
+ grp
179
+ gzip
180
+ hashlib
181
+ heapq
182
+ hmac
183
+ html
184
+ http
185
+ idlelib
186
+ imaplib
187
+ imghdr
188
+ imp
189
+ importlib
190
+ inspect
191
+ io
192
+ ipaddress
193
+ itertools
194
+ json
195
+ keyword
196
+ lib2to3
197
+ linecache
198
+ locale
199
+ logging
200
+ lzma
201
+ mailbox
202
+ mailcap
203
+ marshal
204
+ math
205
+ memray
206
+ mimetypes
207
+ mmap
208
+ modulefinder
209
+ multiprocessing
210
+ netrc
211
+ nis
212
+ nntplib
213
+ ntpath
214
+ nturl2path
215
+ numbers
216
+ opcode
217
+ operator
218
+ optparse
219
+ os
220
+ parser
221
+ pathlib
222
+ pdb
223
+ pickle
224
+ pickletools
225
+ pip
226
+ pipes
227
+ pkg_resources
228
+ pkgutil
229
+ platform
230
+ plistlib
231
+ poplib
232
+ posix
233
+ posixpath
234
+ pprint
235
+ profile
236
+ pstats
237
+ pty
238
+ pwd
239
+ py_compile
240
+ pyclbr
241
+ pydoc
242
+ pydoc_data
243
+ pyexpat
244
+ queue
245
+ quopri
246
+ random
247
+ re
248
+ readline
249
+ reprlib
250
+ resource
251
+ rlcompleter
252
+ runpy
253
+ sched
254
+ secrets
255
+ select
256
+ selectors
257
+ setuptools
258
+ shelve
259
+ shlex
260
+ shutil
261
+ signal
262
+ site
263
+ smtpd
264
+ smtplib
265
+ sndhdr
266
+ socket
267
+ socketserver
268
+ sqlite3
269
+ sre_compile
270
+ sre_constants
271
+ sre_parse
272
+ ssl
273
+ stat
274
+ statistics
275
+ string
276
+ stringprep
277
+ struct
278
+ subprocess
279
+ sunau
280
+ symbol
281
+ symtable
282
+ sys
283
+ sysconfig
284
+ syslog
285
+ tabnanny
286
+ tarfile
287
+ telnetlib
288
+ tempfile
289
+ termios
290
+ test
291
+ textwrap
292
+ this
293
+ threading
294
+ time
295
+ timeit
296
+ tkinter
297
+ token
298
+ tokenize
299
+ trace
300
+ traceback
301
+ tracemalloc
302
+ tty
303
+ turtle
304
+ turtledemo
305
+ types
306
+ typing
307
+ unicodedata
308
+ unittest
309
+ urllib
310
+ uu
311
+ uuid
312
+ venv
313
+ warnings
314
+ wave
315
+ weakref
316
+ webbrowser
317
+ wheel
318
+ wsgiref
319
+ xdrlib
320
+ xml
321
+ xmlrpc
322
+ xxlimited
323
+ xxsubtype
324
+ zipapp
325
+ zipfile
326
+ zipimport
327
+ zlib
328
+ zoneinfo""".split()
329
+ )
@@ -0,0 +1,144 @@
1
+ """
2
+ Library for check_dependencies
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import ast
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any, Callable, List, Optional, Sequence, cast
13
+
14
+ import toml
15
+
16
+ logger = logging.getLogger("check_dependencies.lib")
17
+
18
+
19
+ class Dependency(Enum):
20
+ """Possible depdendency state"""
21
+
22
+ NA = "!" # Not Available
23
+ EXTRA = "+" # Extra dependency in config file
24
+ OK = " " # Correct import (declared in config file)
25
+
26
+
27
+ @dataclass()
28
+ class Config:
29
+ """Application config and helper functions"""
30
+
31
+ file: Optional[str] = None
32
+ include_dev: bool = False
33
+ verbose: bool = False
34
+ show_all: bool = False
35
+ extra_requirements: Sequence[str] = ()
36
+ ignore_requirements: Sequence[str] = ()
37
+
38
+ def __post_init__(self) -> None:
39
+ self.extra_requirements = list(filter(None, self.extra_requirements or []))
40
+ self.ignore_requirements = list(filter(None, self.ignore_requirements or []))
41
+ if self.file and Path(self.file).is_dir():
42
+ self.file = (Path(self.file) / "pyproject.toml").as_posix()
43
+ if self.file:
44
+ try:
45
+ cfg = toml.load(Path(self.file))
46
+ except FileNotFoundError:
47
+ logger.fatal("Could not find config file: %s. Set to None", self.file)
48
+ raise
49
+
50
+ extra_cfg = _nested_item(cfg, "tool.check_dependencies.extra-requirements")
51
+ self.extra_requirements += cast(List[str], list(extra_cfg or []))
52
+
53
+ ignore_cfg = _nested_item(
54
+ cfg, "tool.check_dependencies.ignore-requirements"
55
+ )
56
+ self.ignore_requirements += cast(List[str], list(ignore_cfg))
57
+ else:
58
+ logger.info("Config file unset, showing all imports")
59
+ self.show_all = True
60
+ self.include_dev = False
61
+ self.include_dev = False
62
+ self.ignore_requirements = ()
63
+ self.extra_requirements = ()
64
+
65
+ def get_declared_dependencies(self) -> set[str]:
66
+ """
67
+ Get dependencies from pyproject.toml file.
68
+ ! Currently only poetry style dependencies are supported
69
+ """
70
+ if not self.file:
71
+ return set()
72
+ if (cfg_pth := Path(self.file)).is_dir():
73
+ cfg_pth /= "pyproject.toml"
74
+ try:
75
+ cfg = toml.load(cfg_pth)
76
+ except FileNotFoundError:
77
+ return set()
78
+
79
+ deps = set(_nested_item(cfg, "tool.poetry.dependencies"))
80
+ if self_name := _nested_item(cfg, "tool.poetry.name"):
81
+ cast(List[str], self.extra_requirements).append(_canonical_pkg(self_name))
82
+ if self.include_dev:
83
+ deps |= set(_nested_item(cfg, "tool.poetry.group.dev.dependencies"))
84
+ deps |= set(_nested_item(cfg, "tool.poetry.dev-dependencies"))
85
+ return {_canonical_pkg(x) for x in deps} - {"python"}
86
+
87
+ def mk_src_formatter(
88
+ self,
89
+ ) -> Callable[[str, Dependency, str, Optional[ast.stmt]], Optional[str]]:
90
+ """Formatter for missing or used dependencies"""
91
+ cache: set[str] = set()
92
+
93
+ def src_cause_formatter(
94
+ file: str, cause: Dependency, module: str, stmt: Optional[ast.stmt]
95
+ ) -> Optional[str]:
96
+ if not self.file:
97
+ if self.verbose:
98
+ location = f"{file}:{getattr(stmt, 'lineno', -1)}"
99
+ return f"{location} {module}"
100
+ if (pkg_ := pkg(module)) not in cache:
101
+ cache.add(pkg_)
102
+ return pkg_
103
+ if self.verbose:
104
+ location = f"{file}:{getattr(stmt, 'lineno', -1)}"
105
+ cause_str = f"{cause.value}{cause.name}" if self.file else ""
106
+ if self.show_all or not self.file or cause == Dependency.NA:
107
+ return f"{cause_str} {location} {module}"
108
+ else:
109
+ if (pkg_ := pkg(module)) not in cache:
110
+ cache.add(pkg_)
111
+ cause_str = cause.value if self.file else " "
112
+ if self.show_all or not self.file or cause == Dependency.NA:
113
+ return f"{cause_str} {pkg_}"
114
+ return None
115
+
116
+ return src_cause_formatter
117
+
118
+ def mk_unused_formatter(self) -> Callable[[str], Optional[str]]:
119
+ """Formatter for unused but declared dependencies"""
120
+
121
+ def unused_formatter(module: str) -> Optional[str]:
122
+ if not self.file:
123
+ return None
124
+ if self.verbose:
125
+ return f"{Dependency.EXTRA.value}{Dependency.EXTRA.name} {module}"
126
+ return f"{Dependency.EXTRA.value} {module}"
127
+
128
+ return unused_formatter
129
+
130
+
131
+ def pkg(module: str) -> str:
132
+ """Get the installable module name from an import statement"""
133
+ return module.split(".")[0]
134
+
135
+
136
+ def _nested_item(obj: dict[str, Any], keys: str) -> Any:
137
+ """Get items from a nested dictionary where the keys are dot-separated"""
138
+ for a in keys.split("."):
139
+ obj = obj.get(a, {})
140
+ return obj
141
+
142
+
143
+ def _canonical_pkg(name: str) -> str:
144
+ return name.replace("-", "_")
@@ -0,0 +1,105 @@
1
+ """Main module for check_dependencies"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Generator, Iterator, Sequence
9
+
10
+ from check_dependencies.builtin_module import BUILTINS
11
+ from check_dependencies.lib import Config, Dependency, pkg
12
+
13
+ logger = logging.getLogger("check_dependencies")
14
+
15
+
16
+ def yield_wrong_imports(
17
+ file_names: Sequence[str], cfg: Config
18
+ ) -> Generator[str, None, int]:
19
+ """Yield output lines of missing/unused imports (or all imports in case of cfg.show_all)"""
20
+ declared_deps = cfg.get_declared_dependencies()
21
+ used_deps: set[str] = set()
22
+ missing_fmt = cfg.mk_src_formatter()
23
+ exit_status = 0
24
+ for file_name in _collect_files(file_names):
25
+ for cause, module, stmt in _missing_imports_iter(
26
+ Path(file_name),
27
+ declared_deps | BUILTINS | set(cfg.extra_requirements),
28
+ seen=used_deps,
29
+ ):
30
+ if cause != Dependency.OK:
31
+ exit_status |= 2
32
+ used_deps.add(pkg(module))
33
+ if f := missing_fmt(file_name, cause, module, stmt):
34
+ yield f
35
+
36
+ if cfg.file:
37
+ unused_fmt = cfg.mk_unused_formatter()
38
+ errs = [
39
+ err
40
+ for dep in sorted(declared_deps - used_deps - set(cfg.ignore_requirements))
41
+ if (err := unused_fmt(dep))
42
+ ]
43
+ if errs:
44
+ exit_status |= 4
45
+ if cfg.verbose:
46
+ yield ""
47
+ yield "### Dependencies in config file not used in application:"
48
+ yield f"# Config file: {cfg.file}"
49
+ yield from errs
50
+ return exit_status
51
+
52
+
53
+ def _collect_files(file_names: Sequence[str]) -> Iterator[str]:
54
+ """
55
+ Collect all Python files in a list of files or directories.
56
+ Ensure no file is visited more than once
57
+ """
58
+ seen = set()
59
+ for p in map(Path, file_names):
60
+ if (p_resolved := p.resolve()) in seen:
61
+ continue
62
+ seen.add(p_resolved)
63
+ if p.is_dir():
64
+ for p_sub in p.rglob("*.py"):
65
+ if (p_sub_resolved := p_sub.resolve()) in seen:
66
+ continue
67
+ seen.add(p_sub_resolved)
68
+ yield p_sub.as_posix()
69
+ else:
70
+ yield p.as_posix()
71
+
72
+
73
+ def _missing_imports_iter(
74
+ file: Path, dependencies: set[str], seen: set[str]
75
+ ) -> Iterator[tuple[Dependency, str, ast.stmt]]:
76
+ """
77
+ Find missing imports in a Python file
78
+ :param file: Pyton file to analyse
79
+ :param dependencies: Declared dependencies
80
+ :param seen: Cache of seen packages - this is changed during the iteration
81
+ :yields: Tuple of status, module name and optional import statement
82
+ """
83
+ try:
84
+ parsed = ast.parse(file.read_text(), filename=str(file))
85
+ except SyntaxError as exc:
86
+ logger.error("Could not parse %s: %s", file, exc)
87
+ return
88
+ for module, stmt in _imports_iter(parsed.body):
89
+ pkg_ = pkg(module)
90
+ status = Dependency.OK if pkg_ in dependencies else Dependency.NA
91
+ seen.add(pkg_)
92
+ yield status, module, stmt
93
+
94
+
95
+ def _imports_iter(body: list[ast.stmt]) -> Iterator[tuple[str, ast.stmt]]:
96
+ """Yield all import statements from a body of code"""
97
+ for x in body:
98
+ if isinstance(x, ast.Import):
99
+ for alias in x.names:
100
+ yield alias.name, x # yield x, not alias to get lineno
101
+ elif isinstance(x, ast.ImportFrom) and x.level == 0:
102
+ # level > 0 means relative import
103
+ yield x.module or "", x
104
+ elif hasattr(x, "body"):
105
+ yield from _imports_iter(x.body)