Skip to content

Commit 89f4d18

Browse files
author
Daniel Zagaynov
committed
Merge development branch 'kotopes-develop'
2 parents b083c36 + 5e1352e commit 89f4d18

File tree

5 files changed

+268
-104
lines changed

5 files changed

+268
-104
lines changed

README.md

Lines changed: 182 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,210 @@
11
# Py3DepHell
22
This project presents tools to work with dependencies and provides of python3 projects.
33

4-
## py3req
5-
This module detects dependencies of python3 packages. It has verbose **--help** option, but here is simple example how to use it:
64

75
## py3prov
86
This module generate provides for python3 packages. As for **py3req** its **--help** is verbose enough
97

8+
## py3req
9+
This module detects dependencies of python3 packages. It has verbose **--help** option, but here is simple example how to use it:
10+
1011
## How to
11-
Imagine you have simple project like this one:
12-
```
13-
src/
14-
── pkg1
15-
│   ├── mod1.py
16-
│   └── subpkg
17-
│   └── mod3.py
12+
[Imagine](https://packaging.python.org/en/latest/tutorials/packaging-projects/) you have simple project like this one:
13+
```shell
14+
├── src
15+
│   └── pkg1
16+
│   ├── mod1.py
17+
│   └── subpkg
18+
│   └── mod3.py
1819
└── tests
1920
└── test1.py
2021
```
22+
With the following context:
23+
24+
**src/pkg1/mod1.py**:
25+
```python3
26+
import re
27+
import sys
28+
import os
29+
import os.path
2130

22-
Now you want to detect its dependencies:
31+
import numpy
32+
33+
from .subpkg import mod3
2334
```
24-
% python3 -m py3dephell.py3req --pip_format src
25-
unittest
26-
re
35+
36+
**src/pkg1/subpkg/mod3.py**
37+
```python3
38+
import ast
39+
```
40+
41+
**tests/test1.py**
42+
```python3
43+
import unittest
44+
45+
import pytest
46+
```
47+
48+
### Detecting dependencies
49+
Let's run **py3req** to detect deps for our project:
50+
51+
```shell
52+
% py3req src tests
53+
numpy
54+
pytest
55+
```
56+
57+
Let's turn on verbose mode and check what happened with dependencies:
58+
```shell
59+
% py3req --verbose src tests
60+
py3prov: bad name for provides from path:config-3.12-x86_64-linux-gnu
61+
py3req:/tmp/dummy/src/pkg1/mod1.py: "re" lines:[1] is possibly a self-providing dependency, skip it
62+
py3req:/tmp/dummy/src/pkg1/mod1.py: skipping "sys" lines:[2]
63+
py3req:/tmp/dummy/src/pkg1/mod1.py: "os" lines:[3] is possibly a self-providing dependency, skip it
64+
py3req:/tmp/dummy/src/pkg1/mod1.py: "os.path" lines:[4] is possibly a self-providing dependency, skip it
65+
py3req:/tmp/dummy/src/pkg1/mod1.py: "tmp.dummy.src.pkg1.subpkg" lines:[8] is possibly a self-providing dependency, skip it
66+
py3req:/tmp/dummy/src/pkg1/subpkg/mod3.py: "ast" lines:[1] is possibly a self-providing dependency, skip it
67+
py3req:/tmp/dummy/tests/test1.py: "unittest" lines:[1] is possibly a self-providing dependency, skip it
68+
/tmp/dummy/src/pkg1/mod1.py:numpy
69+
/tmp/dummy/tests/test1.py:pytest
70+
```
71+
72+
As you can see, **py3req** recognised dependency from **src/pkg1/mod1.py** to **src/pkg1/subpkg/mod3.py**, but since it is provided by given file list, **py3req** filtered it out.
73+
74+
#### Filtering dependencies
75+
76+
According to the previouse example, **sys** was not classified as a dependency, because **sys** is built-in module, which is provided by interpreter by itself. So such deps are filtered out by **py3req**. To make it visible for **py3req** use option **--include_built-in**:
77+
78+
```shell
79+
% py3req --include_built-in src tests
80+
sys
81+
numpy
82+
pytest
83+
```
84+
85+
Now let's include dependencies, that are provided by python3 standard library:
86+
87+
```shell
88+
% py3req --include_stdlib src tests
2789
re
90+
numpy
91+
os.path
92+
os
93+
ast
94+
pytest
95+
unittest
2896
```
29-
Feel free to make it more verbose:
97+
98+
But what if we have dependency, that is provided by our environment or another one package, so we want **py3req** to find it and exclude from dependencies? For such problem we have **--add_prov_path** option:
99+
100+
```shell
101+
% py3req --add_prov_path src2 src tests
102+
numpy
30103
```
31-
% python3 -m py3dephell.py3req --pip_format --verbose src
32-
py3prov: detected potential module:src
33-
/tmp/.private/kotopesutility/src/tests/test1.py:unittest
34-
/tmp/.private/kotopesutility/src/pkg1/mod1.py:requests os
35-
/tmp/.private/kotopesutility/src/pkg1/subpkg/mod3.py:re
104+
105+
Where **src2** has the following structure:
106+
```shell
107+
src2
108+
└── pytest
109+
└── __init__.py
36110
```
37-
As you can see, there are some modules from standard library, so let py3req to learn it:
111+
112+
Another way to exclude such dependency is to ignore it manually, using **--ignore_list** option:
113+
```shell
114+
% py3req --ignore_list pytest src tests
115+
numpy
38116
```
39-
% python3 -m py3dephell.py3req --pip_format --add_prov_path /usr/lib64/python3.11 src
40-
requests
117+
118+
#### Context dependencies
119+
120+
Finally, there can be deps, that are hidden inside conditions or function calls. For example:
121+
122+
**anime_dld.py**
123+
```python3
124+
import os
125+
126+
127+
def func():
128+
import pytest
129+
130+
131+
try:
132+
import specific_module
133+
except Exception as ex:
134+
print(f"I'm sorry, but {ex}")
135+
136+
137+
a = int(input())
138+
if a == 10:
139+
import ast
140+
else:
141+
import re
41142
```
42-
That's it! But what if we want to detect its provides, to understand which dependencies it could satisfy? Let's use py3prov!
143+
144+
In general it is impossible to check if condition **a == 10** is True or False. Moreover it is not clear if **specific_module** is really important for such project or not. So, by default **py3req** catch them all:
145+
146+
```shell
147+
% py3req anime_dld.py
148+
pytest
149+
specific_module
43150
```
44-
% python3 -m py3dephell.py3prov src
45-
test1
46-
tests.test1
47-
src.tests.test1
48-
mod1
49-
pkg1.mod1
151+
152+
But it is possible to ignore all deps, that are hidden inside contexts:
153+
```shell
154+
% py3req --exclude_hidden_deps anime_dld.py
155+
%
156+
```
157+
158+
Other options are little bit specific, but there is clear **--help** option output. Please, check it.
159+
160+
161+
### Detecting provides
162+
163+
While dependency is something, that is required (imported) by your project, provides are requirements, that are exported by other projects for yours.
164+
165+
To detect provides for our **src** use **py3prov**:
166+
167+
```shell
168+
% py3prov src
169+
src.pkg1.subpkg.mod3
50170
src.pkg1.mod1
171+
```
172+
173+
To get all possible provides (including even modules) use **--full_mode**:
174+
175+
```shell
176+
% py3prov --full_mode src
51177
mod3
52178
subpkg.mod3
53179
pkg1.subpkg.mod3
54180
src.pkg1.subpkg.mod3
181+
mod1
182+
pkg1.mod1
183+
src.pkg1.mod1
55184
```
56-
Yeah, let's enhance the verbosity level!
185+
186+
But all provides are prefixed by **src**, while your project should install **pkg1** in user system. To remove such prefixes use **--prefixes** option:
187+
188+
```shell
189+
% py3prov --prefixes src src
190+
pkg1.subpkg.mod3
191+
pkg1.mod1
57192
```
58-
% python3 -m py3dephell.py3prov --verbose src/pkg1 src/tests
59-
src/tests:['test1', 'tests.test1', 'src.tests.test1']
60-
src/pkg1:['mod1', 'pkg1.mod1', 'src.pkg1.mod1', 'mod3', 'subpkg.mod3', 'pkg1.subpkg.mod3', 'src.pkg1.subpkg.mod3']
193+
194+
By default **--prefixes** is set to **sys.path**, while **$TMP/env/lib/python3/site-packages/** is included in **sys.path**.
195+
196+
```shell
197+
% py3prov $TMP/env/lib/python3/site-packages/py3dephell
198+
py3dephell.__init__
199+
py3dephell
200+
py3dephell.py3prov
201+
py3dephell.py3req
61202
```
203+
204+
205+
206+
Other options, such as **--only_prefix** and **--skip_pth** are little bit specific, but it is clear, what they can be used for. **--only_prefix** exclude those provides, that are not under prefixes. **--skip_pth** ignore [**.pth**](https://docs.python.org/3/library/site.html) files
207+
208+
209+
# API documentation
210+
For **API** documentation just use **help** command from interpreter or visit this [link](https://altlinux.github.io/py3dephell/).

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "py3dephell"
3-
version = "0.2.1"
3+
version = "0.3.1"
44
authors = [
55
{name = "Daniel Zagaynov", email = "[email protected]"},
66
]
@@ -23,7 +23,7 @@ requires = ["setuptools"]
2323
build-backend = "setuptools.build_meta"
2424

2525
[project.urls]
26-
Homepage = "https://git.altlinux.org/people/kotopesutility/packages/py3dephell.git"
26+
Homepage = "https://github.com/altlinux/py3dephell.git"
2727

2828
[project.scripts]
2929
py3req = "py3dephell.py3req:main"

src/py3dephell/py3prov.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def processing_pth(path):
4242

4343

4444
def create_provides_from_path(path, prefixes=sys.path, abs_mode=False,
45-
pkg_mode=False, skip_wrong_names=True, skip_namespace_pkgs=True):
45+
pkg_mode=False, skip_wrong_names=True, skip_namespace_pkgs=True, verbose=False,
46+
_bad_provides=set()):
4647
'''
4748
Creates provides from given path for 1 file.
4849
@@ -58,6 +59,8 @@ def create_provides_from_path(path, prefixes=sys.path, abs_mode=False,
5859
:type skip_wrong_names: Bool
5960
:param skip_namespace_pkgs: do not build provides for namespace packages
6061
:type skip_namespace_pkgs: Bool
62+
:param verbose: turn on verbose mode
63+
:type verbose: Bool
6164
:return: list of provides created from given path
6265
:rtype: list[str]
6366
'''
@@ -97,7 +100,9 @@ def create_provides_from_path(path, prefixes=sys.path, abs_mode=False,
97100
top_package_flag = True
98101

99102
if '.' in parts[-1]:
100-
print(f'py3prov: bad name for provides from path:{path.as_posix()}', file=sys.stderr)
103+
if parts[-1] not in _bad_provides and verbose:
104+
print(f'py3prov: bad name for provides from path:{path.as_posix()}', file=sys.stderr)
105+
_bad_provides.add(parts[-1])
101106

102107
if abs_mode and (all([part.isidentifier() for part in parts]) or not skip_wrong_names):
103108
provides.append('.'.join(parts))
@@ -114,13 +119,15 @@ def create_provides_from_path(path, prefixes=sys.path, abs_mode=False,
114119

115120
if (top_package_flag or not skip_namespace_pkgs) and parent.as_posix() != '.':
116121
provides += create_provides_from_path(parent, prefixes,
117-
pkg_mode=True, abs_mode=abs_mode, skip_wrong_names=skip_wrong_names)
122+
pkg_mode=True, abs_mode=abs_mode, skip_wrong_names=skip_wrong_names,
123+
verbose=verbose, _bad_provides=_bad_provides)
118124

119125
return provides
120126

121127

122128
def search_for_provides(path, prefixes=sys.path, abs_mode=False,
123-
skip_wrong_names=True, skip_namespace_pkgs=True):
129+
skip_wrong_names=True, skip_namespace_pkgs=True, verbose=False,
130+
_bad_provides=set()):
124131
'''
125132
This function walks through given path and search for provides
126133
@@ -134,6 +141,8 @@ def search_for_provides(path, prefixes=sys.path, abs_mode=False,
134141
:type skip_wrong_names: Bool
135142
:param skip_namespace_pkgs: do not build provides for namespace packages
136143
:type skip_namespace_pkgs: Bool
144+
:param verbose: turn on verbose mode
145+
:type verbose: Bool
137146
:return: list of provides created from given path
138147
:rtype: list[str]
139148
'''
@@ -142,10 +151,12 @@ def search_for_provides(path, prefixes=sys.path, abs_mode=False,
142151

143152
if path.is_file() or path.is_symlink():
144153
return create_provides_from_path(path.as_posix(), prefixes, abs_mode=abs_mode,
145-
skip_wrong_names=skip_wrong_names, skip_namespace_pkgs=skip_namespace_pkgs)
154+
skip_wrong_names=skip_wrong_names, skip_namespace_pkgs=skip_namespace_pkgs,
155+
verbose=verbose, _bad_provides=_bad_provides)
146156
elif path.is_dir() and '__pycache__' not in path.as_posix():
147157
for subpath in path.iterdir():
148-
provides += search_for_provides(subpath, prefixes, abs_mode, skip_wrong_names, skip_namespace_pkgs)
158+
provides += search_for_provides(subpath, prefixes, abs_mode, skip_wrong_names, skip_namespace_pkgs,
159+
verbose=verbose, _bad_provides=_bad_provides)
149160
return provides
150161

151162

@@ -287,7 +298,8 @@ def generate_provides(files, prefixes=sys.path, skip_pth=False, only_prefix=Fals
287298
for path, module_name in files_dict.items():
288299
provides[path] = {'provides': search_for_provides(path, prefixes, abs_mode=abs_mode,
289300
skip_wrong_names=skip_wrong_names,
290-
skip_namespace_pkgs=skip_namespace_pkgs),
301+
skip_namespace_pkgs=skip_namespace_pkgs,
302+
_bad_provides=set(), verbose=verbose),
291303
'package': module_name}
292304

293305
if not skip_pth:
@@ -313,8 +325,8 @@ def generate_provides(files, prefixes=sys.path, skip_pth=False, only_prefix=Fals
313325
def main():
314326
args = argparse.ArgumentParser(description='Search provides for module')
315327
args.add_argument('--prefixes', help='List of prefixes')
316-
args.add_argument('--abs_mode', action='store_true',
317-
help='Turn on plugin mode (build only absolute provides)')
328+
args.add_argument('--full_mode', action='store_true',
329+
help='Build all provides, not just absolute')
318330
args.add_argument('--only_prefix', action='store_true',
319331
help='Skip all provides, that are not in prefix')
320332
args.add_argument('--skip_pth', action='store_true', help='Skip pth files')
@@ -329,7 +341,7 @@ def main():
329341
prefixes = args.prefixes.split(',') if args.prefixes else sys.path
330342

331343
path_provides = generate_provides(files=args.input, prefixes=prefixes,
332-
skip_pth=args.skip_pth, abs_mode=args.abs_mode,
344+
skip_pth=args.skip_pth, abs_mode=not args.full_mode,
333345
only_prefix=args.only_prefix, verbose=args.verbose)
334346
for path, provides in path_provides.items():
335347
if args.verbose:

0 commit comments

Comments
 (0)