py2docfx 0.1.20rc2245107__py3-none-any.whl → 0.1.21__py3-none-any.whl
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.
- py2docfx/convert_prepare/get_source.py +1 -1
- py2docfx/convert_prepare/package_info.py +37 -27
- py2docfx/convert_prepare/tests/test_get_source.py +3 -1
- py2docfx/convert_prepare/tests/test_package_info.py +159 -1
- py2docfx/docfx_yaml/build_finished.py +1 -1
- py2docfx/docfx_yaml/logger.py +42 -28
- py2docfx/venv/venv1/Lib/site-packages/psutil/__init__.py +39 -19
- py2docfx/venv/venv1/Lib/site-packages/psutil/_common.py +3 -5
- py2docfx/venv/venv1/Lib/site-packages/psutil/_psaix.py +1 -2
- py2docfx/venv/venv1/Lib/site-packages/psutil/_psbsd.py +53 -78
- py2docfx/venv/venv1/Lib/site-packages/psutil/_pslinux.py +55 -38
- py2docfx/venv/venv1/Lib/site-packages/psutil/_psosx.py +40 -12
- py2docfx/venv/venv1/Lib/site-packages/psutil/_psposix.py +0 -1
- py2docfx/venv/venv1/Lib/site-packages/psutil/_pssunos.py +1 -2
- py2docfx/venv/venv1/Lib/site-packages/psutil/_pswindows.py +33 -13
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__init__.py +185 -122
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__main__.py +2 -3
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_bsd.py +5 -10
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_connections.py +3 -4
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_contracts.py +41 -45
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_linux.py +35 -38
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_memleaks.py +4 -8
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_misc.py +6 -12
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_osx.py +17 -8
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_posix.py +29 -17
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process.py +74 -75
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process_all.py +11 -13
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_scripts.py +2 -3
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_sudo.py +117 -0
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_system.py +21 -31
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_testutils.py +23 -23
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_unicode.py +15 -8
- py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_windows.py +65 -33
- {py2docfx-0.1.20rc2245107.dist-info → py2docfx-0.1.21.dist-info}/METADATA +1 -1
- {py2docfx-0.1.20rc2245107.dist-info → py2docfx-0.1.21.dist-info}/RECORD +37 -36
- {py2docfx-0.1.20rc2245107.dist-info → py2docfx-0.1.21.dist-info}/WHEEL +0 -0
- {py2docfx-0.1.20rc2245107.dist-info → py2docfx-0.1.21.dist-info}/top_level.txt +0 -0
@@ -54,7 +54,7 @@ def update_package_info(executable: str, pkg: PackageInfo, source_folder: str):
|
|
54
54
|
for meta_info in metadata:
|
55
55
|
meta_info_array = meta_info.split(":")
|
56
56
|
meta_field = meta_info_array[0].strip().lower()
|
57
|
-
if meta_field in attrs:
|
57
|
+
if meta_field in attrs and not hasattr(pkg, meta_field):
|
58
58
|
setattr(
|
59
59
|
pkg,
|
60
60
|
meta_field,
|
@@ -13,7 +13,7 @@ class PackageInfo:
|
|
13
13
|
path: Source
|
14
14
|
def __init__(self) -> None:
|
15
15
|
pass
|
16
|
-
|
16
|
+
|
17
17
|
@classmethod
|
18
18
|
def report_error(cls, name, value, condition=None):
|
19
19
|
py2docfx_logger = get_logger(__name__)
|
@@ -53,6 +53,7 @@ class PackageInfo:
|
|
53
53
|
|
54
54
|
package_info.version = package_info_dict.get("version", None)
|
55
55
|
package_info.extra_index_url = package_info_dict.get("extra_index_url", None)
|
56
|
+
package_info.extras = package_info_dict.get("extras", [])
|
56
57
|
|
57
58
|
if package_info.install_type == cls.InstallType.SOURCE_CODE:
|
58
59
|
package_info.url = package_info_dict.get("url", None)
|
@@ -93,56 +94,65 @@ class PackageInfo:
|
|
93
94
|
return package_info
|
94
95
|
|
95
96
|
def get_combined_name_version(self):
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
return f"{self.name.strip()}=={self.version.strip()}"
|
97
|
+
base_name = (
|
98
|
+
f"{self.name}[{','.join(extras)}]"
|
99
|
+
if (extras := getattr(self, "extras", []))
|
100
|
+
else self.name
|
101
|
+
)
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
103
|
+
version = getattr(self, "version", None)
|
104
|
+
if not version:
|
105
|
+
return base_name
|
106
|
+
elif re.match("^(<|>|<=|>=|==).+$", version.strip()):
|
107
|
+
return base_name.strip() + version.strip()
|
108
|
+
else:
|
109
|
+
return f"{base_name.strip()}=={version.strip()}"
|
106
110
|
|
111
|
+
def get_install_command(self) -> tuple[str, list]:
|
107
112
|
if self.install_type == self.InstallType.DIST_FILE:
|
108
|
-
if hasattr(self, "location")
|
109
|
-
packageInstallName = self.location
|
110
|
-
else:
|
113
|
+
if not hasattr(self, "location") or not self.location:
|
111
114
|
self.__class__.report_error(
|
112
115
|
"location", "None", condition="When install_type is dist_file"
|
113
116
|
)
|
117
|
+
return (
|
118
|
+
f"{self.location}[{','.join(extras)}]"
|
119
|
+
if (extras := getattr(self, "extras", None))
|
120
|
+
else self.location,
|
121
|
+
[],
|
122
|
+
)
|
114
123
|
|
115
|
-
|
124
|
+
if self.install_type == self.InstallType.PYPI:
|
116
125
|
if not hasattr(self, "name") or not self.name:
|
117
126
|
self.__class__.report_error(
|
118
127
|
"name", "None", condition="When install_type is pypi"
|
119
128
|
)
|
120
|
-
|
121
|
-
|
122
|
-
else:
|
123
|
-
packageInstallName = self.name
|
129
|
+
pipInstallExtraOptions = []
|
130
|
+
if not hasattr(self, "version") or self.version is None:
|
124
131
|
pipInstallExtraOptions.append("--upgrade")
|
125
|
-
|
126
132
|
if hasattr(self, "extra_index_url") and self.extra_index_url:
|
127
133
|
pipInstallExtraOptions.extend(
|
128
134
|
["--extra-index-url", self.extra_index_url]
|
129
135
|
)
|
136
|
+
return (self.get_combined_name_version(), pipInstallExtraOptions)
|
130
137
|
|
131
|
-
|
132
|
-
if hasattr(self, "path")
|
133
|
-
packageInstallName = self.path.source_folder
|
134
|
-
else:
|
138
|
+
if self.install_type == self.InstallType.SOURCE_CODE:
|
139
|
+
if not hasattr(self, "path") or not self.path.source_folder:
|
135
140
|
self.__class__.report_error(
|
136
141
|
"path.source_folder",
|
137
142
|
"None",
|
138
143
|
condition="When install_type is source_code",
|
139
144
|
)
|
140
|
-
|
141
|
-
|
145
|
+
return (
|
146
|
+
f"{self.path.source_folder}[{','.join(extras)}]"
|
147
|
+
if (extras := getattr(self, "extras", None))
|
148
|
+
else self.path.source_folder,
|
149
|
+
[],
|
150
|
+
)
|
151
|
+
|
152
|
+
self.__class__.report_error("install_type", self.install_type)
|
142
153
|
|
143
|
-
return (packageInstallName, pipInstallExtraOptions)
|
144
154
|
|
145
|
-
def get_exluded_command(self) ->
|
155
|
+
def get_exluded_command(self) -> list:
|
146
156
|
py2docfx_logger = get_logger(__name__)
|
147
157
|
if hasattr(self, "path"):
|
148
158
|
code_location = self.path.source_folder
|
@@ -50,8 +50,10 @@ def test_update_package_info(init_package_info):
|
|
50
50
|
assert package.name == "dummy_package"
|
51
51
|
assert package.version == "3.1.0"
|
52
52
|
|
53
|
-
# case of metadata
|
53
|
+
# case of metadata, unly use metadata file as a fallback
|
54
54
|
package = init_package_info
|
55
|
+
del package.name
|
56
|
+
del package.version
|
55
57
|
get_source.update_package_info(sys.executable, package, os.path.join(base_path, "mock-2"))
|
56
58
|
assert package.name == "mock_package"
|
57
59
|
assert package.version == "2.2.0"
|
@@ -80,4 +80,162 @@ def test_get_exclude_command_check_extra_exclude(tmp_path):
|
|
80
80
|
]
|
81
81
|
def form_exclude_path(raletive_path):
|
82
82
|
return os.path.join(source_folder, raletive_path)
|
83
|
-
assert exclude_path == [form_exclude_path(path) for path in expected_exclude_path]
|
83
|
+
assert exclude_path == [form_exclude_path(path) for path in expected_exclude_path]
|
84
|
+
|
85
|
+
|
86
|
+
def test_get_combined_name_version_with_extras():
|
87
|
+
"""Test get_combined_name_version with extras"""
|
88
|
+
# Test package with extras but no version
|
89
|
+
test_data = {
|
90
|
+
"package_info": {
|
91
|
+
"install_type": "pypi",
|
92
|
+
"name": "test-package",
|
93
|
+
"extras": ["dev", "test"],
|
94
|
+
},
|
95
|
+
}
|
96
|
+
pkg = PackageInfo.parse_from(test_data)
|
97
|
+
assert pkg.get_combined_name_version() == "test-package[dev,test]"
|
98
|
+
|
99
|
+
# Test package with extras and version
|
100
|
+
test_data_with_version = {
|
101
|
+
"package_info": {
|
102
|
+
"install_type": "pypi",
|
103
|
+
"name": "test-package",
|
104
|
+
"version": "1.0.0",
|
105
|
+
"extras": ["dev", "test"],
|
106
|
+
},
|
107
|
+
}
|
108
|
+
pkg_with_version = PackageInfo.parse_from(test_data_with_version)
|
109
|
+
assert (
|
110
|
+
pkg_with_version.get_combined_name_version() == "test-package[dev,test]==1.0.0"
|
111
|
+
)
|
112
|
+
|
113
|
+
# Test package with extras and version operator
|
114
|
+
test_data_with_operator = {
|
115
|
+
"package_info": {
|
116
|
+
"install_type": "pypi",
|
117
|
+
"name": "test-package",
|
118
|
+
"version": ">=1.0.0",
|
119
|
+
"extras": ["dev"],
|
120
|
+
},
|
121
|
+
}
|
122
|
+
pkg_with_operator = PackageInfo.parse_from(test_data_with_operator)
|
123
|
+
assert pkg_with_operator.get_combined_name_version() == "test-package[dev]>=1.0.0"
|
124
|
+
|
125
|
+
|
126
|
+
def test_install_command_pypi_with_extras():
|
127
|
+
"""Test get_install_command for PYPI packages with extras"""
|
128
|
+
# Test PYPI package with extras and version
|
129
|
+
test_data = {
|
130
|
+
"package_info": {
|
131
|
+
"install_type": "pypi",
|
132
|
+
"name": "test-package",
|
133
|
+
"version": "1.0.0",
|
134
|
+
"extras": ["dev", "test"],
|
135
|
+
},
|
136
|
+
}
|
137
|
+
pkg = PackageInfo.parse_from(test_data)
|
138
|
+
install_command = pkg.get_install_command()
|
139
|
+
assert install_command[0] == "test-package[dev,test]==1.0.0"
|
140
|
+
assert install_command[1] == []
|
141
|
+
|
142
|
+
# Test PYPI package with extras but no version (should get --upgrade)
|
143
|
+
test_data_no_version = {
|
144
|
+
"package_info": {
|
145
|
+
"install_type": "pypi",
|
146
|
+
"name": "test-package",
|
147
|
+
"extras": ["dev"],
|
148
|
+
},
|
149
|
+
}
|
150
|
+
pkg_no_version = PackageInfo.parse_from(test_data_no_version)
|
151
|
+
install_command = pkg_no_version.get_install_command()
|
152
|
+
assert install_command[0] == "test-package[dev]"
|
153
|
+
assert install_command[1] == ["--upgrade"]
|
154
|
+
|
155
|
+
|
156
|
+
def test_install_command_source_code_with_extras(tmp_path):
|
157
|
+
"""Test get_install_command for SOURCE_CODE packages with extras"""
|
158
|
+
source_folder = os.path.join(tmp_path, "source_folder")
|
159
|
+
yaml_output_folder = os.path.join(tmp_path, "yaml_output_folder")
|
160
|
+
|
161
|
+
test_data = {
|
162
|
+
"package_info": {
|
163
|
+
"install_type": "source_code",
|
164
|
+
"name": "test-package",
|
165
|
+
"url": "https://github.com/test/test-package.git",
|
166
|
+
"extras": ["dev", "test"],
|
167
|
+
},
|
168
|
+
}
|
169
|
+
pkg = PackageInfo.parse_from(test_data)
|
170
|
+
pkg.path = Source(
|
171
|
+
source_folder=source_folder,
|
172
|
+
yaml_output_folder=yaml_output_folder,
|
173
|
+
package_name="test-package",
|
174
|
+
)
|
175
|
+
|
176
|
+
install_command = pkg.get_install_command()
|
177
|
+
assert install_command[0] == f"{source_folder}[dev,test]"
|
178
|
+
assert install_command[1] == []
|
179
|
+
|
180
|
+
|
181
|
+
def test_install_command_dist_file_with_extras():
|
182
|
+
"""Test get_install_command for DIST_FILE packages with extras"""
|
183
|
+
test_data = {
|
184
|
+
"package_info": {
|
185
|
+
"install_type": "dist_file",
|
186
|
+
"location": "/path/to/package.whl",
|
187
|
+
"extras": ["dev"],
|
188
|
+
},
|
189
|
+
}
|
190
|
+
pkg = PackageInfo.parse_from(test_data)
|
191
|
+
install_command = pkg.get_install_command()
|
192
|
+
assert install_command[0] == "/path/to/package.whl[dev]"
|
193
|
+
assert install_command[1] == []
|
194
|
+
|
195
|
+
|
196
|
+
def test_install_command_without_extras():
|
197
|
+
"""Test that packages without extras work as before"""
|
198
|
+
# Test PYPI package without extras
|
199
|
+
test_data = {
|
200
|
+
"package_info": {
|
201
|
+
"install_type": "pypi",
|
202
|
+
"name": "test-package",
|
203
|
+
"version": "1.0.0",
|
204
|
+
}
|
205
|
+
}
|
206
|
+
pkg = PackageInfo.parse_from(test_data)
|
207
|
+
install_command = pkg.get_install_command()
|
208
|
+
assert install_command[0] == "test-package==1.0.0"
|
209
|
+
assert install_command[1] == []
|
210
|
+
|
211
|
+
|
212
|
+
def test_install_command_empty_extras():
|
213
|
+
"""Test that packages with empty extras list work correctly"""
|
214
|
+
test_data = {
|
215
|
+
"package_info": {
|
216
|
+
"install_type": "pypi",
|
217
|
+
"name": "test-package",
|
218
|
+
"version": "1.0.0",
|
219
|
+
"extras": [],
|
220
|
+
},
|
221
|
+
}
|
222
|
+
pkg = PackageInfo.parse_from(test_data)
|
223
|
+
install_command = pkg.get_install_command()
|
224
|
+
assert install_command[0] == "test-package==1.0.0"
|
225
|
+
assert install_command[1] == []
|
226
|
+
|
227
|
+
|
228
|
+
def test_install_command_single_extra():
|
229
|
+
"""Test package with single extra"""
|
230
|
+
test_data = {
|
231
|
+
"package_info": {
|
232
|
+
"install_type": "pypi",
|
233
|
+
"name": "test-package",
|
234
|
+
"version": "1.0.0",
|
235
|
+
"extras": ["dev"],
|
236
|
+
},
|
237
|
+
}
|
238
|
+
pkg = PackageInfo.parse_from(test_data)
|
239
|
+
install_command = pkg.get_install_command()
|
240
|
+
assert install_command[0] == "test-package[dev]==1.0.0"
|
241
|
+
assert install_command[1] == []
|
@@ -277,7 +277,7 @@ def build_finished(app, exception):
|
|
277
277
|
obj['kind'] = 'import'
|
278
278
|
package_obj = obj
|
279
279
|
|
280
|
-
if (obj['type'] == 'class' and
|
280
|
+
if (obj['type'] == 'class' and 'inheritance' in obj):
|
281
281
|
convert_class_to_enum_if_needed(obj)
|
282
282
|
|
283
283
|
is_root = insert_node_to_toc_tree_return_is_root_package(toc_yaml, uid, project_name, toc_node_map)
|
py2docfx/docfx_yaml/logger.py
CHANGED
@@ -77,18 +77,18 @@ def counts_errors_warnings(log_file_path):
|
|
77
77
|
warning_count += 1
|
78
78
|
return warning_count, error_count
|
79
79
|
|
80
|
-
def get_warning_error_count():
|
81
|
-
main_log_file_path = os.path.join("logs", "log.txt")
|
82
|
-
warning_count, error_count = counts_errors_warnings(main_log_file_path)
|
83
|
-
|
84
|
-
log_folder_path = os.path.join("logs", "package_logs")
|
85
|
-
# Check if the directory exists before trying to list its contents
|
86
|
-
if os.path.exists(log_folder_path) and os.path.isdir(log_folder_path):
|
87
|
-
for log_file in os.listdir(log_folder_path):
|
88
|
-
log_file_path = os.path.join(log_folder_path, log_file)
|
89
|
-
warnings, errors = counts_errors_warnings(log_file_path)
|
90
|
-
warning_count += warnings
|
91
|
-
error_count += errors
|
80
|
+
def get_warning_error_count():
|
81
|
+
main_log_file_path = os.path.join("logs", "log.txt")
|
82
|
+
warning_count, error_count = counts_errors_warnings(main_log_file_path)
|
83
|
+
|
84
|
+
log_folder_path = os.path.join("logs", "package_logs")
|
85
|
+
# Check if the directory exists before trying to list its contents
|
86
|
+
if os.path.exists(log_folder_path) and os.path.isdir(log_folder_path):
|
87
|
+
for log_file in os.listdir(log_folder_path):
|
88
|
+
log_file_path = os.path.join(log_folder_path, log_file)
|
89
|
+
warnings, errors = counts_errors_warnings(log_file_path)
|
90
|
+
warning_count += warnings
|
91
|
+
error_count += errors
|
92
92
|
|
93
93
|
return warning_count, error_count
|
94
94
|
|
@@ -120,16 +120,16 @@ def print_out_log_by_log_level(log_list, log_level):
|
|
120
120
|
if log['level'] >= log_level and log['message'] not in ['', '\n', '\r\n']:
|
121
121
|
print(log['message'])
|
122
122
|
|
123
|
-
def output_log_by_log_level():
|
124
|
-
log_level = get_log_level()
|
125
|
-
main_log_file_path = os.path.join("logs", "log.txt")
|
126
|
-
print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
|
127
|
-
|
128
|
-
package_logs_folder = os.path.join("logs", "package_logs")
|
129
|
-
# Check if the directory exists before trying to list its contents
|
130
|
-
if os.path.exists(package_logs_folder) and os.path.isdir(package_logs_folder):
|
131
|
-
for log_file in os.listdir(package_logs_folder):
|
132
|
-
log_file_path = os.path.join(package_logs_folder, log_file)
|
123
|
+
def output_log_by_log_level():
|
124
|
+
log_level = get_log_level()
|
125
|
+
main_log_file_path = os.path.join("logs", "log.txt")
|
126
|
+
print_out_log_by_log_level(parse_log(main_log_file_path), log_level)
|
127
|
+
|
128
|
+
package_logs_folder = os.path.join("logs", "package_logs")
|
129
|
+
# Check if the directory exists before trying to list its contents
|
130
|
+
if os.path.exists(package_logs_folder) and os.path.isdir(package_logs_folder):
|
131
|
+
for log_file in os.listdir(package_logs_folder):
|
132
|
+
log_file_path = os.path.join(package_logs_folder, log_file)
|
133
133
|
print_out_log_by_log_level(parse_log(log_file_path), log_level)
|
134
134
|
|
135
135
|
async def run_async_subprocess(exe_path, cmd, logger, cwd=None):
|
@@ -148,9 +148,16 @@ async def run_async_subprocess(exe_path, cmd, logger, cwd=None):
|
|
148
148
|
)
|
149
149
|
stdout, stderr = await process.communicate()
|
150
150
|
if process.returncode != 0:
|
151
|
-
|
152
|
-
|
153
|
-
|
151
|
+
# Log both stdout and stderr on failure - pip often outputs detailed
|
152
|
+
# dependency resolution errors to stdout even when it fails
|
153
|
+
stdout_msg = stdout.decode('utf-8')
|
154
|
+
stderr_msg = stderr.decode('utf-8')
|
155
|
+
|
156
|
+
if stdout_msg and stdout_msg.strip():
|
157
|
+
logger.error(f"STDOUT: {stdout_msg}")
|
158
|
+
if stderr_msg and stderr_msg.strip():
|
159
|
+
logger.error(f"STDERR: {stderr_msg}")
|
160
|
+
|
154
161
|
raise subprocess.CalledProcessError(process.returncode, cmd, stdout, stderr)
|
155
162
|
else:
|
156
163
|
msg = stdout.decode('utf-8')
|
@@ -174,9 +181,16 @@ async def run_async_subprocess_without_executable(cmd, logger, cwd=None):
|
|
174
181
|
|
175
182
|
stdout, stderr = await process.communicate()
|
176
183
|
if process.returncode != 0:
|
177
|
-
|
178
|
-
|
179
|
-
|
184
|
+
# Log both stdout and stderr on failure - pip often outputs detailed
|
185
|
+
# dependency resolution errors to stdout even when it fails
|
186
|
+
stdout_msg = stdout.decode('utf-8')
|
187
|
+
stderr_msg = stderr.decode('utf-8')
|
188
|
+
|
189
|
+
if stdout_msg and stdout_msg.strip():
|
190
|
+
logger.error(f"STDOUT: {stdout_msg}")
|
191
|
+
if stderr_msg and stderr_msg.strip():
|
192
|
+
logger.error(f"STDERR: {stderr_msg}")
|
193
|
+
|
180
194
|
raise subprocess.CalledProcessError(process.returncode, cmd, stdout, stderr)
|
181
195
|
else:
|
182
196
|
msg = stdout.decode('utf-8')
|
@@ -30,7 +30,6 @@ import sys
|
|
30
30
|
import threading
|
31
31
|
import time
|
32
32
|
|
33
|
-
|
34
33
|
try:
|
35
34
|
import pwd
|
36
35
|
except ImportError:
|
@@ -86,7 +85,6 @@ from ._common import debug
|
|
86
85
|
from ._common import memoize_when_activated
|
87
86
|
from ._common import wrap_numbers as _wrap_numbers
|
88
87
|
|
89
|
-
|
90
88
|
if LINUX:
|
91
89
|
# This is public API and it will be retrieved from _pslinux.py
|
92
90
|
# via sys.modules.
|
@@ -207,7 +205,7 @@ if hasattr(_psplatform.Process, "rlimit"):
|
|
207
205
|
AF_LINK = _psplatform.AF_LINK
|
208
206
|
|
209
207
|
__author__ = "Giampaolo Rodola'"
|
210
|
-
__version__ = "7.
|
208
|
+
__version__ = "7.1.0"
|
211
209
|
version_info = tuple(int(num) for num in __version__.split('.'))
|
212
210
|
|
213
211
|
_timer = getattr(time, 'monotonic', time.time)
|
@@ -377,7 +375,11 @@ class Process:
|
|
377
375
|
won't reuse the same PID after such a short period of time
|
378
376
|
(0.01 secs). Technically this is inherently racy, but
|
379
377
|
practically it should be good enough.
|
378
|
+
|
379
|
+
NOTE: unreliable on FreeBSD and OpenBSD as ctime is subject to
|
380
|
+
system clock updates.
|
380
381
|
"""
|
382
|
+
|
381
383
|
if WINDOWS:
|
382
384
|
# Use create_time() fast method in order to speedup
|
383
385
|
# `process_iter()`. This means we'll get AccessDenied for
|
@@ -386,6 +388,11 @@ class Process:
|
|
386
388
|
# https://github.com/giampaolo/psutil/issues/2366#issuecomment-2381646555
|
387
389
|
self._create_time = self._proc.create_time(fast_only=True)
|
388
390
|
return (self.pid, self._create_time)
|
391
|
+
elif LINUX or NETBSD or OSX:
|
392
|
+
# Use 'monotonic' process starttime since boot to form unique
|
393
|
+
# process identity, since it is stable over changes to system
|
394
|
+
# time.
|
395
|
+
return (self.pid, self._proc.create_time(monotonic=True))
|
389
396
|
else:
|
390
397
|
return (self.pid, self.create_time())
|
391
398
|
|
@@ -426,12 +433,12 @@ class Process:
|
|
426
433
|
# on PID and creation time.
|
427
434
|
if not isinstance(other, Process):
|
428
435
|
return NotImplemented
|
429
|
-
if OPENBSD or NETBSD: # pragma: no cover
|
430
|
-
# Zombie processes on Open/NetBSD have a
|
431
|
-
# 0.0.
|
432
|
-
# (so it has a ctime), then it turned into a
|
433
|
-
# important to do this because is_running()
|
434
|
-
# __eq__.
|
436
|
+
if OPENBSD or NETBSD or SUNOS: # pragma: no cover
|
437
|
+
# Zombie processes on Open/NetBSD/illumos/Solaris have a
|
438
|
+
# creation time of 0.0. This covers the case when a process
|
439
|
+
# started normally (so it has a ctime), then it turned into a
|
440
|
+
# zombie. It's important to do this because is_running()
|
441
|
+
# depends on __eq__.
|
435
442
|
pid1, ident1 = self._ident
|
436
443
|
pid2, ident2 = other._ident
|
437
444
|
if pid1 == pid2:
|
@@ -593,10 +600,13 @@ class Process:
|
|
593
600
|
return None
|
594
601
|
ppid = self.ppid()
|
595
602
|
if ppid is not None:
|
596
|
-
|
603
|
+
# Get a fresh (non-cached) ctime in case the system clock
|
604
|
+
# was updated. TODO: use a monotonic ctime on platforms
|
605
|
+
# where it's supported.
|
606
|
+
proc_ctime = Process(self.pid).create_time()
|
597
607
|
try:
|
598
608
|
parent = Process(ppid)
|
599
|
-
if parent.create_time() <=
|
609
|
+
if parent.create_time() <= proc_ctime:
|
600
610
|
return parent
|
601
611
|
# ...else ppid has been reused by another process
|
602
612
|
except NoSuchProcess:
|
@@ -765,8 +775,11 @@ class Process:
|
|
765
775
|
|
766
776
|
def create_time(self):
|
767
777
|
"""The process creation time as a floating point number
|
768
|
-
expressed in seconds since the epoch
|
769
|
-
The return value is cached after
|
778
|
+
expressed in seconds since the epoch (seconds since January 1,
|
779
|
+
1970, at midnight UTC). The return value, which is cached after
|
780
|
+
first call, is based on the system clock, which means it may be
|
781
|
+
affected by changes such as manual adjustments or time
|
782
|
+
synchronization (e.g. NTP).
|
770
783
|
"""
|
771
784
|
if self._create_time is None:
|
772
785
|
self._create_time = self._proc.create_time()
|
@@ -964,6 +977,10 @@ class Process:
|
|
964
977
|
"""
|
965
978
|
self._raise_if_pid_reused()
|
966
979
|
ppid_map = _ppid_map()
|
980
|
+
# Get a fresh (non-cached) ctime in case the system clock was
|
981
|
+
# updated. TODO: use a monotonic ctime on platforms where it's
|
982
|
+
# supported.
|
983
|
+
proc_ctime = Process(self.pid).create_time()
|
967
984
|
ret = []
|
968
985
|
if not recursive:
|
969
986
|
for pid, ppid in ppid_map.items():
|
@@ -972,7 +989,7 @@ class Process:
|
|
972
989
|
child = Process(pid)
|
973
990
|
# if child happens to be older than its parent
|
974
991
|
# (self) it means child's PID has been reused
|
975
|
-
if
|
992
|
+
if proc_ctime <= child.create_time():
|
976
993
|
ret.append(child)
|
977
994
|
except (NoSuchProcess, ZombieProcess):
|
978
995
|
pass
|
@@ -998,7 +1015,7 @@ class Process:
|
|
998
1015
|
child = Process(child_pid)
|
999
1016
|
# if child happens to be older than its parent
|
1000
1017
|
# (self) it means child's PID has been reused
|
1001
|
-
intime =
|
1018
|
+
intime = proc_ctime <= child.create_time()
|
1002
1019
|
if intime:
|
1003
1020
|
ret.append(child)
|
1004
1021
|
stack.append(child_pid)
|
@@ -1484,7 +1501,7 @@ def process_iter(attrs=None, ad_value=None):
|
|
1484
1501
|
|
1485
1502
|
Every new Process instance is only created once and then cached
|
1486
1503
|
into an internal table which is updated every time this is used.
|
1487
|
-
Cache can optionally be cleared via `process_iter.
|
1504
|
+
Cache can optionally be cleared via `process_iter.cache_clear()`.
|
1488
1505
|
|
1489
1506
|
The sorting order in which processes are yielded is based on
|
1490
1507
|
their PIDs.
|
@@ -2352,9 +2369,12 @@ if hasattr(_psplatform, "sensors_battery"):
|
|
2352
2369
|
|
2353
2370
|
|
2354
2371
|
def boot_time():
|
2355
|
-
"""Return the system boot time expressed in seconds since the epoch
|
2356
|
-
|
2357
|
-
|
2372
|
+
"""Return the system boot time expressed in seconds since the epoch
|
2373
|
+
(seconds since January 1, 1970, at midnight UTC). The returned
|
2374
|
+
value is based on the system clock, which means it may be affected
|
2375
|
+
by changes such as manual adjustments or time synchronization (e.g.
|
2376
|
+
NTP).
|
2377
|
+
"""
|
2358
2378
|
return _psplatform.boot_time()
|
2359
2379
|
|
2360
2380
|
|
@@ -22,7 +22,6 @@ from socket import AF_INET
|
|
22
22
|
from socket import SOCK_DGRAM
|
23
23
|
from socket import SOCK_STREAM
|
24
24
|
|
25
|
-
|
26
25
|
try:
|
27
26
|
from socket import AF_INET6
|
28
27
|
except ImportError:
|
@@ -409,7 +408,7 @@ def memoize(fun):
|
|
409
408
|
except KeyError:
|
410
409
|
try:
|
411
410
|
ret = cache[key] = fun(*args, **kwargs)
|
412
|
-
except Exception as err:
|
411
|
+
except Exception as err:
|
413
412
|
raise err from None
|
414
413
|
return ret
|
415
414
|
|
@@ -458,14 +457,14 @@ def memoize_when_activated(fun):
|
|
458
457
|
# case 2: we never entered oneshot() ctx
|
459
458
|
try:
|
460
459
|
return fun(self)
|
461
|
-
except Exception as err:
|
460
|
+
except Exception as err:
|
462
461
|
raise err from None
|
463
462
|
except KeyError:
|
464
463
|
# case 3: we entered oneshot() ctx but there's no cache
|
465
464
|
# for this entry yet
|
466
465
|
try:
|
467
466
|
ret = fun(self)
|
468
|
-
except Exception as err:
|
467
|
+
except Exception as err:
|
469
468
|
raise err from None
|
470
469
|
try:
|
471
470
|
self._cache[fun] = ret
|
@@ -523,7 +522,6 @@ def path_exists_strict(path):
|
|
523
522
|
return True
|
524
523
|
|
525
524
|
|
526
|
-
@memoize
|
527
525
|
def supports_ipv6():
|
528
526
|
"""Return True if IPv6 is supported on this platform."""
|
529
527
|
if not socket.has_ipv6 or AF_INET6 is None:
|
@@ -29,7 +29,6 @@ from ._common import get_procfs_path
|
|
29
29
|
from ._common import memoize_when_activated
|
30
30
|
from ._common import usage_percent
|
31
31
|
|
32
|
-
|
33
32
|
__extra__all__ = ["PROCFS_PATH"]
|
34
33
|
|
35
34
|
|
@@ -279,7 +278,7 @@ def boot_time():
|
|
279
278
|
def users():
|
280
279
|
"""Return currently connected users as a list of namedtuples."""
|
281
280
|
retlist = []
|
282
|
-
rawlist =
|
281
|
+
rawlist = cext_posix.users()
|
283
282
|
localhost = (':0.0', ':0')
|
284
283
|
for item in rawlist:
|
285
284
|
user, tty, hostname, tstamp, user_process, pid = item
|