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.
- check_dependencies-0.9.0/LICENSE +21 -0
- check_dependencies-0.9.0/PKG-INFO +125 -0
- check_dependencies-0.9.0/README.md +108 -0
- check_dependencies-0.9.0/pyproject.toml +33 -0
- check_dependencies-0.9.0/src/check_dependencies/__init__.py +0 -0
- check_dependencies-0.9.0/src/check_dependencies/__main__.py +84 -0
- check_dependencies-0.9.0/src/check_dependencies/builtin_module.py +329 -0
- check_dependencies-0.9.0/src/check_dependencies/lib.py +144 -0
- check_dependencies-0.9.0/src/check_dependencies/main.py +105 -0
|
@@ -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"
|
|
File without changes
|
|
@@ -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)
|