py2docfx 0.1.20.dev2245319__py3-none-any.whl → 0.1.20.dev2246325__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.
Files changed (34) hide show
  1. py2docfx/convert_prepare/package_info.py +27 -37
  2. py2docfx/convert_prepare/pip_utils.py +1 -3
  3. py2docfx/convert_prepare/tests/test_package_info.py +1 -159
  4. py2docfx/venv/venv1/Lib/site-packages/psutil/__init__.py +39 -19
  5. py2docfx/venv/venv1/Lib/site-packages/psutil/_common.py +3 -5
  6. py2docfx/venv/venv1/Lib/site-packages/psutil/_psaix.py +1 -2
  7. py2docfx/venv/venv1/Lib/site-packages/psutil/_psbsd.py +53 -78
  8. py2docfx/venv/venv1/Lib/site-packages/psutil/_pslinux.py +55 -38
  9. py2docfx/venv/venv1/Lib/site-packages/psutil/_psosx.py +40 -12
  10. py2docfx/venv/venv1/Lib/site-packages/psutil/_psposix.py +0 -1
  11. py2docfx/venv/venv1/Lib/site-packages/psutil/_pssunos.py +1 -2
  12. py2docfx/venv/venv1/Lib/site-packages/psutil/_pswindows.py +33 -13
  13. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__init__.py +185 -122
  14. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/__main__.py +2 -3
  15. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_bsd.py +5 -10
  16. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_connections.py +3 -4
  17. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_contracts.py +41 -45
  18. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_linux.py +35 -38
  19. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_memleaks.py +4 -8
  20. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_misc.py +6 -12
  21. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_osx.py +17 -8
  22. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_posix.py +29 -17
  23. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process.py +74 -75
  24. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_process_all.py +11 -13
  25. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_scripts.py +2 -3
  26. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_sudo.py +117 -0
  27. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_system.py +21 -31
  28. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_testutils.py +23 -23
  29. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_unicode.py +15 -8
  30. py2docfx/venv/venv1/Lib/site-packages/psutil/tests/test_windows.py +65 -33
  31. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2246325.dist-info}/METADATA +1 -1
  32. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2246325.dist-info}/RECORD +34 -33
  33. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2246325.dist-info}/WHEEL +0 -0
  34. {py2docfx-0.1.20.dev2245319.dist-info → py2docfx-0.1.20.dev2246325.dist-info}/top_level.txt +0 -0
@@ -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,7 +53,6 @@ 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", [])
57
56
 
58
57
  if package_info.install_type == cls.InstallType.SOURCE_CODE:
59
58
  package_info.url = package_info_dict.get("url", None)
@@ -94,65 +93,56 @@ class PackageInfo:
94
93
  return package_info
95
94
 
96
95
  def get_combined_name_version(self):
97
- base_name = (
98
- f"{self.name}[{','.join(extras)}]"
99
- if (extras := getattr(self, "extras", []))
100
- else self.name
101
- )
102
-
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()
96
+ if not self.version:
97
+ return self.name
98
+ elif re.match("^(<|>|<=|>=|==).+$", self.version.strip()):
99
+ return self.name.strip() + self.version.strip()
108
100
  else:
109
- return f"{base_name.strip()}=={version.strip()}"
101
+ return f"{self.name.strip()}=={self.version.strip()}"
102
+
103
+ def get_install_command(self) -> (str, []):
104
+ packageInstallName = ""
105
+ pipInstallExtraOptions = []
110
106
 
111
- def get_install_command(self) -> tuple[str, list]:
112
107
  if self.install_type == self.InstallType.DIST_FILE:
113
- if not hasattr(self, "location") or not self.location:
108
+ if hasattr(self, "location") and self.location:
109
+ packageInstallName = self.location
110
+ else:
114
111
  self.__class__.report_error(
115
112
  "location", "None", condition="When install_type is dist_file"
116
113
  )
117
- return (
118
- f"{self.location}[{','.join(extras)}]"
119
- if (extras := getattr(self, "extras", None))
120
- else self.location,
121
- [],
122
- )
123
114
 
124
- if self.install_type == self.InstallType.PYPI:
115
+ elif self.install_type == self.InstallType.PYPI:
125
116
  if not hasattr(self, "name") or not self.name:
126
117
  self.__class__.report_error(
127
118
  "name", "None", condition="When install_type is pypi"
128
119
  )
129
- pipInstallExtraOptions = []
130
- if not hasattr(self, "version") or self.version is None:
120
+ if hasattr(self, "version") and self.version:
121
+ packageInstallName = self.get_combined_name_version()
122
+ else:
123
+ packageInstallName = self.name
131
124
  pipInstallExtraOptions.append("--upgrade")
125
+
132
126
  if hasattr(self, "extra_index_url") and self.extra_index_url:
133
127
  pipInstallExtraOptions.extend(
134
128
  ["--extra-index-url", self.extra_index_url]
135
129
  )
136
- return (self.get_combined_name_version(), pipInstallExtraOptions)
137
130
 
138
- if self.install_type == self.InstallType.SOURCE_CODE:
139
- if not hasattr(self, "path") or not self.path.source_folder:
131
+ elif self.install_type == self.InstallType.SOURCE_CODE:
132
+ if hasattr(self, "path") and self.path.source_folder:
133
+ packageInstallName = self.path.source_folder
134
+ else:
140
135
  self.__class__.report_error(
141
136
  "path.source_folder",
142
137
  "None",
143
138
  condition="When install_type is source_code",
144
139
  )
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)
140
+ else:
141
+ self.__class__.report_error("install_type", self.install_type)
153
142
 
143
+ return (packageInstallName, pipInstallExtraOptions)
154
144
 
155
- def get_exluded_command(self) -> list:
145
+ def get_exluded_command(self) -> []:
156
146
  py2docfx_logger = get_logger(__name__)
157
147
  if hasattr(self, "path"):
158
148
  code_location = self.path.source_folder
@@ -7,11 +7,9 @@ PYPI = "pypi"
7
7
 
8
8
  pip_install_common_options = [
9
9
  # "--no-cache-dir", # to reduce install duration after switching to each venv
10
- "--quiet",
11
10
  "--no-compile",
12
- "--no-warn-conflicts",
13
11
  "--disable-pip-version-check",
14
- "--verbose"
12
+ "-vvv"
15
13
  ]
16
14
 
17
15
  async def download(package_name, path, extra_index_url=None, prefer_source_distribution=True):
@@ -80,162 +80,4 @@ 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]
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] == []
83
+ assert exclude_path == [form_exclude_path(path) for path in expected_exclude_path]
@@ -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.0.0"
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 creation time of
431
- # 0.0. This covers the case when a process started normally
432
- # (so it has a ctime), then it turned into a zombie. It's
433
- # important to do this because is_running() depends on
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
- ctime = self.create_time()
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() <= ctime:
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 first call.
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 self.create_time() <= child.create_time():
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 = self.create_time() <= child.create_time()
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.clear_cache()`.
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
- # Note: we are not caching this because it is subject to
2357
- # system clock updates.
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: # noqa: BLE001
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: # noqa: BLE001
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: # noqa: BLE001
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 = cext.users()
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
@@ -29,7 +29,6 @@ from ._common import memoize
29
29
  from ._common import memoize_when_activated
30
30
  from ._common import usage_percent
31
31
 
32
-
33
32
  __extra__all__ = []
34
33
 
35
34
 
@@ -98,10 +97,7 @@ TCP_STATUSES = {
98
97
  PAGESIZE = cext_posix.getpagesize()
99
98
  AF_LINK = cext_posix.AF_LINK
100
99
 
101
- HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times")
102
100
  HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads")
103
- HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files')
104
- HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds')
105
101
 
106
102
  kinfo_proc_map = dict(
107
103
  ppid=0,
@@ -240,36 +236,14 @@ def cpu_times():
240
236
  return scputimes(user, nice, system, idle, irq)
241
237
 
242
238
 
243
- if HAS_PER_CPU_TIMES:
244
-
245
- def per_cpu_times():
246
- """Return system CPU times as a namedtuple."""
247
- ret = []
248
- for cpu_t in cext.per_cpu_times():
249
- user, nice, system, idle, irq = cpu_t
250
- item = scputimes(user, nice, system, idle, irq)
251
- ret.append(item)
252
- return ret
253
-
254
- else:
255
- # XXX
256
- # Ok, this is very dirty.
257
- # On FreeBSD < 8 we cannot gather per-cpu information, see:
258
- # https://github.com/giampaolo/psutil/issues/226
259
- # If num cpus > 1, on first call we return single cpu times to avoid a
260
- # crash at psutil import time.
261
- # Next calls will fail with NotImplementedError
262
- def per_cpu_times():
263
- """Return system CPU times as a namedtuple."""
264
- if cpu_count_logical() == 1:
265
- return [cpu_times()]
266
- if per_cpu_times.__called__:
267
- msg = "supported only starting from FreeBSD 8"
268
- raise NotImplementedError(msg)
269
- per_cpu_times.__called__ = True
270
- return [cpu_times()]
271
-
272
- per_cpu_times.__called__ = False
239
+ def per_cpu_times():
240
+ """Return system CPU times as a namedtuple."""
241
+ ret = []
242
+ for cpu_t in cext.per_cpu_times():
243
+ user, nice, system, idle, irq = cpu_t
244
+ item = scputimes(user, nice, system, idle, irq)
245
+ ret.append(item)
246
+ return ret
273
247
 
274
248
 
275
249
  def cpu_count_logical():
@@ -505,15 +479,36 @@ def boot_time():
505
479
  return cext.boot_time()
506
480
 
507
481
 
482
+ if NETBSD:
483
+
484
+ try:
485
+ INIT_BOOT_TIME = boot_time()
486
+ except Exception as err: # noqa: BLE001
487
+ # Don't want to crash at import time.
488
+ debug(f"ignoring exception on import: {err!r}")
489
+ INIT_BOOT_TIME = 0
490
+
491
+ def adjust_proc_create_time(ctime):
492
+ """Account for system clock updates."""
493
+ if INIT_BOOT_TIME == 0:
494
+ return ctime
495
+
496
+ diff = INIT_BOOT_TIME - boot_time()
497
+ if diff == 0 or abs(diff) < 1:
498
+ return ctime
499
+
500
+ debug("system clock was updated; adjusting process create_time()")
501
+ if diff < 0:
502
+ return ctime - diff
503
+ return ctime + diff
504
+
505
+
508
506
  def users():
509
507
  """Return currently connected users as a list of namedtuples."""
510
508
  retlist = []
511
- rawlist = cext.users()
509
+ rawlist = cext.users() if OPENBSD else cext_posix.users()
512
510
  for item in rawlist:
513
511
  user, tty, hostname, tstamp, pid = item
514
- if pid == -1:
515
- assert OPENBSD
516
- pid = None
517
512
  if tty == '~':
518
513
  continue # reboot or shutdown
519
514
  nt = _common.suser(user, tty or None, hostname, tstamp, pid)
@@ -779,13 +774,17 @@ class Process:
779
774
  memory_full_info = memory_info
780
775
 
781
776
  @wrap_exceptions
782
- def create_time(self):
783
- return self.oneshot()[kinfo_proc_map['create_time']]
777
+ def create_time(self, monotonic=False):
778
+ ctime = self.oneshot()[kinfo_proc_map['create_time']]
779
+ if NETBSD and not monotonic:
780
+ # NetBSD: ctime subject to system clock updates.
781
+ ctime = adjust_proc_create_time(ctime)
782
+ return ctime
784
783
 
785
784
  @wrap_exceptions
786
785
  def num_threads(self):
787
786
  if HAS_PROC_NUM_THREADS:
788
- # FreeBSD
787
+ # FreeBSD / NetBSD
789
788
  return cext.proc_num_threads(self.pid)
790
789
  else:
791
790
  return len(self.threads())
@@ -870,14 +869,7 @@ class Process:
870
869
  # it into None
871
870
  if OPENBSD and self.pid == 0:
872
871
  return "" # ...else it would raise EINVAL
873
- elif NETBSD or HAS_PROC_OPEN_FILES:
874
- # FreeBSD < 8 does not support functions based on
875
- # kinfo_getfile() and kinfo_getvmmap()
876
- return cext.proc_cwd(self.pid)
877
- else:
878
- raise NotImplementedError(
879
- "supported only starting from FreeBSD 8" if FREEBSD else ""
880
- )
872
+ return cext.proc_cwd(self.pid)
881
873
 
882
874
  nt_mmap_grouped = namedtuple(
883
875
  'mmap', 'path rss, private, ref_count, shadow_count'
@@ -886,36 +878,19 @@ class Process:
886
878
  'mmap', 'addr, perms path rss, private, ref_count, shadow_count'
887
879
  )
888
880
 
889
- def _not_implemented(self):
890
- raise NotImplementedError
891
-
892
- # FreeBSD < 8 does not support functions based on kinfo_getfile()
893
- # and kinfo_getvmmap()
894
- if HAS_PROC_OPEN_FILES:
895
-
896
- @wrap_exceptions
897
- def open_files(self):
898
- """Return files opened by process as a list of namedtuples."""
899
- rawlist = cext.proc_open_files(self.pid)
900
- return [_common.popenfile(path, fd) for path, fd in rawlist]
901
-
902
- else:
903
- open_files = _not_implemented
904
-
905
- # FreeBSD < 8 does not support functions based on kinfo_getfile()
906
- # and kinfo_getvmmap()
907
- if HAS_PROC_NUM_FDS:
908
-
909
- @wrap_exceptions
910
- def num_fds(self):
911
- """Return the number of file descriptors opened by this process."""
912
- ret = cext.proc_num_fds(self.pid)
913
- if NETBSD:
914
- self._assert_alive()
915
- return ret
881
+ @wrap_exceptions
882
+ def open_files(self):
883
+ """Return files opened by process as a list of namedtuples."""
884
+ rawlist = cext.proc_open_files(self.pid)
885
+ return [_common.popenfile(path, fd) for path, fd in rawlist]
916
886
 
917
- else:
918
- num_fds = _not_implemented
887
+ @wrap_exceptions
888
+ def num_fds(self):
889
+ """Return the number of file descriptors opened by this process."""
890
+ ret = cext.proc_num_fds(self.pid)
891
+ if NETBSD:
892
+ self._assert_alive()
893
+ return ret
919
894
 
920
895
  # --- FreeBSD only APIs
921
896