extra-platforms 1.0.0__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.
@@ -0,0 +1,29 @@
1
+ # Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
2
+ #
3
+ # This program is Free Software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU General Public License
5
+ # as published by the Free Software Foundation; either version 2
6
+ # of the License, or (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
+ """Expose package-wide elements."""
17
+
18
+ __version__ = "1.0.0"
19
+ """Examples of valid version strings according :pep:`440#version-scheme`:
20
+
21
+ .. code-block:: python
22
+
23
+ __version__ = "1.2.3.dev1" # Development release 1
24
+ __version__ = "1.2.3a1" # Alpha Release 1
25
+ __version__ = "1.2.3b1" # Beta Release 1
26
+ __version__ = "1.2.3rc1" # RC Release 1
27
+ __version__ = "1.2.3" # Final Release
28
+ __version__ = "1.2.3.post1" # Post Release 1
29
+ """
@@ -0,0 +1,972 @@
1
+ # Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
2
+ #
3
+ # This program is Free Software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU General Public License
5
+ # as published by the Free Software Foundation; either version 2
6
+ # of the License, or (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program; if not, write to the Free Software
15
+ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
+ """Helpers and utilities to identify platforms.
17
+
18
+ Everything here can be aggressively cached and frozen, as it's only compute
19
+ platform-dependent values.
20
+
21
+ .. note::
22
+
23
+ Default icons are inspired from Starship project:
24
+ - https://starship.rs/config/#os
25
+ - https://github.com/davidkna/starship/blob/e9faf17/.github/config-schema.json#L1221-L1269
26
+
27
+
28
+ .. note::
29
+
30
+ Heuristics for unrecognized platforms can be found in `Rust's sysinfo crate
31
+ <https://github.com/stanislav-tkach/os_info/tree/master/os_info/src>`_.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import platform
37
+ import sys
38
+ import warnings
39
+ from dataclasses import dataclass, field
40
+ from itertools import combinations
41
+ from os import environ
42
+ from typing import Any, Iterable, Iterator
43
+
44
+ import distro
45
+ from boltons.iterutils import remap
46
+
47
+ from . import cache
48
+
49
+ """ Below is the collection of heuristics used to identify each platform.
50
+
51
+ All these heuristics can be hard-cached as the underlying system is not suppose to
52
+ change between code execution.
53
+
54
+ We mostly rely on ``sys.platform`` first as it seems to be the lowest-level primitive
55
+ available to identify systems.
56
+
57
+ We choose to have separate function to detect each platform so we can easely check
58
+ consistency. It helps ensure there is no heuristics conflicting and matching multiple
59
+ systems at the same time.
60
+ """
61
+
62
+
63
+ @cache
64
+ def is_aix() -> bool:
65
+ """Return `True` only if current platform is AIX."""
66
+ return sys.platform.startswith("aix") or distro.id() == "aix"
67
+
68
+
69
+ @cache
70
+ def is_altlinux() -> bool:
71
+ """Return `True` only if current platform is ALT Linux."""
72
+ return distro.id() == "altlinux"
73
+
74
+
75
+ @cache
76
+ def is_amzn() -> bool:
77
+ """Return `True` only if current platform is Amazon Linux."""
78
+ return distro.id() == "amzn"
79
+
80
+
81
+ @cache
82
+ def is_android() -> bool:
83
+ """Return `True` only if current platform is Android.
84
+
85
+ Source: https://github.com/kivy/kivy/blob/master/kivy/utils.py#L429
86
+ """
87
+ return "ANDROID_ROOT" in environ or "P4A_BOOTSTRAP" in environ
88
+
89
+
90
+ @cache
91
+ def is_arch() -> bool:
92
+ """Return `True` only if current platform is Arch Linux."""
93
+ return distro.id() == "arch"
94
+
95
+
96
+ @cache
97
+ def is_buildroot() -> bool:
98
+ """Return `True` only if current platform is Buildroot."""
99
+ return distro.id() == "buildroot"
100
+
101
+
102
+ @cache
103
+ def is_centos() -> bool:
104
+ """Return `True` only if current platform is CentOS."""
105
+ return distro.id() == "centos"
106
+
107
+
108
+ @cache
109
+ def is_cloudlinux() -> bool:
110
+ """Return `True` only if current platform is CloudLinux OS."""
111
+ return distro.id() == "cloudlinux"
112
+
113
+
114
+ @cache
115
+ def is_cygwin() -> bool:
116
+ """Return `True` only if current platform is Cygwin."""
117
+ return sys.platform.startswith("cygwin")
118
+
119
+
120
+ @cache
121
+ def is_debian() -> bool:
122
+ """Return `True` only if current platform is Debian."""
123
+ return distro.id() == "debian"
124
+
125
+
126
+ @cache
127
+ def is_exherbo() -> bool:
128
+ """Return `True` only if current platform is Exherbo Linux."""
129
+ return distro.id() == "exherbo"
130
+
131
+
132
+ @cache
133
+ def is_fedora() -> bool:
134
+ """Return `True` only if current platform is Fedora."""
135
+ return distro.id() == "fedora"
136
+
137
+
138
+ @cache
139
+ def is_freebsd() -> bool:
140
+ """Return `True` only if current platform is FreeBSD."""
141
+ return sys.platform.startswith("freebsd") or distro.id() == "freebsd"
142
+
143
+
144
+ @cache
145
+ def is_gentoo() -> bool:
146
+ """Return `True` only if current platform is GenToo Linux."""
147
+ return distro.id() == "gentoo"
148
+
149
+
150
+ @cache
151
+ def is_guix() -> bool:
152
+ """Return `True` only if current platform is Guix System."""
153
+ return distro.id() == "guix"
154
+
155
+
156
+ @cache
157
+ def is_hurd() -> bool:
158
+ """Return `True` only if current platform is GNU/Hurd."""
159
+ return sys.platform.startswith("GNU")
160
+
161
+
162
+ @cache
163
+ def is_ibm_powerkvm() -> bool:
164
+ """Return `True` only if current platform is IBM PowerKVM."""
165
+ return distro.id() == "ibm_powerkvm"
166
+
167
+
168
+ @cache
169
+ def is_kvmibm() -> bool:
170
+ """Return `True` only if current platform is KVM for IBM z Systems."""
171
+ return distro.id() == "kvmibm"
172
+
173
+
174
+ @cache
175
+ def is_linux() -> bool:
176
+ """ """
177
+ warnings.warn(
178
+ "is_linux() is a covenient method that has been deprecated by the recent "
179
+ "introduction of fine-grained distribution identification",
180
+ DeprecationWarning,
181
+ )
182
+ return CURRENT_OS_ID in ALL_LINUX.platform_ids
183
+
184
+
185
+ @cache
186
+ def is_linuxmint() -> bool:
187
+ """Return `True` only if current platform is Linux Mint."""
188
+ return distro.id() == "linuxmint"
189
+
190
+
191
+ @cache
192
+ def is_macos() -> bool:
193
+ """Return `True` only if current platform is macOS."""
194
+ return platform.platform(terse=True).startswith(("macOS", "Darwin"))
195
+
196
+
197
+ @cache
198
+ def is_mageia() -> bool:
199
+ """Return `True` only if current platform is Mageia."""
200
+ return distro.id() == "mageia"
201
+
202
+
203
+ @cache
204
+ def is_mandriva() -> bool:
205
+ """Return `True` only if current platform is Mandriva Linux."""
206
+ return distro.id() == "mandriva"
207
+
208
+
209
+ @cache
210
+ def is_midnightbsd() -> bool:
211
+ """Return `True` only if current platform is MidnightBSD."""
212
+ return sys.platform.startswith("midnightbsd") or distro.id() == "midnightbsd"
213
+
214
+
215
+ @cache
216
+ def is_netbsd() -> bool:
217
+ """Return `True` only if current platform is NetBSD."""
218
+ return sys.platform.startswith("netbsd") or distro.id() == "netbsd"
219
+
220
+
221
+ @cache
222
+ def is_openbsd() -> bool:
223
+ """Return `True` only if current platform is OpenBSD."""
224
+ return sys.platform.startswith("openbsd") or distro.id() == "openbsd"
225
+
226
+
227
+ @cache
228
+ def is_opensuse() -> bool:
229
+ """Return `True` only if current platform is openSUSE."""
230
+ return distro.id() == "opensuse"
231
+
232
+
233
+ @cache
234
+ def is_oracle() -> bool:
235
+ """Return `True` only if current platform is Oracle Linux (and Oracle Enterprise Linux)."""
236
+ return distro.id() == "oracle"
237
+
238
+
239
+ @cache
240
+ def is_parallels() -> bool:
241
+ """Return `True` only if current platform is Parallels."""
242
+ return distro.id() == "parallels"
243
+
244
+
245
+ @cache
246
+ def is_pidora() -> bool:
247
+ """Return `True` only if current platform is Pidora."""
248
+ return distro.id() == "pidora"
249
+
250
+
251
+ @cache
252
+ def is_raspbian() -> bool:
253
+ """Return `True` only if current platform is Raspbian."""
254
+ return distro.id() == "raspbian"
255
+
256
+
257
+ @cache
258
+ def is_rhel() -> bool:
259
+ """Return `True` only if current platform is RedHat Enterprise Linux."""
260
+ return distro.id() == "rhel"
261
+
262
+
263
+ @cache
264
+ def is_rocky() -> bool:
265
+ """Return `True` only if current platform is Rocky Linux."""
266
+ return distro.id() == "rocky"
267
+
268
+
269
+ @cache
270
+ def is_scientific() -> bool:
271
+ """Return `True` only if current platform is Scientific Linux."""
272
+ return distro.id() == "scientific"
273
+
274
+
275
+ @cache
276
+ def is_slackware() -> bool:
277
+ """Return `True` only if current platform is Slackware."""
278
+ return distro.id() == "slackware"
279
+
280
+
281
+ @cache
282
+ def is_sles() -> bool:
283
+ """Return `True` only if current platform is SUSE Linux Enterprise Server."""
284
+ return distro.id() == "sles"
285
+
286
+
287
+ @cache
288
+ def is_solaris() -> bool:
289
+ """Return `True` only if current platform is Solaris."""
290
+ return platform.platform(aliased=True, terse=True).startswith("Solaris")
291
+
292
+
293
+ @cache
294
+ def is_sunos() -> bool:
295
+ """Return `True` only if current platform is SunOS."""
296
+ return platform.platform(aliased=True, terse=True).startswith("SunOS")
297
+
298
+
299
+ @cache
300
+ def is_ubuntu() -> bool:
301
+ """Return `True` only if current platform is Ubuntu."""
302
+ return distro.id() == "ubuntu"
303
+
304
+
305
+ @cache
306
+ def is_unknown_linux() -> bool:
307
+ """Return `True` only if current platform is an unknown Linux.
308
+
309
+ Excludes WSL1 and WSL2 from this check to
310
+ `avoid false positives <https://github.com/kdeldycke/meta-package-manager/issues/944>`_.
311
+ """
312
+ return sys.platform.startswith("linux") and not (
313
+ is_ubuntu() or is_wsl1() or is_wsl2()
314
+ )
315
+
316
+
317
+ @cache
318
+ def is_windows() -> bool:
319
+ """Return `True` only if current platform is Windows."""
320
+ return sys.platform.startswith("win32")
321
+
322
+
323
+ @cache
324
+ def is_wsl1() -> bool:
325
+ """Return `True` only if current platform is Windows Subsystem for Linux v1.
326
+
327
+ .. caution::
328
+ The only difference between WSL1 and WSL2 is `the case of the kernel release
329
+ version <https://github.com/andweeb/presence.nvim/pull/64#issue-1174430662>`_:
330
+
331
+ - WSL 1:
332
+
333
+ .. code-block:: shell-session
334
+
335
+ $ uname -r
336
+ 4.4.0-22572-Microsoft
337
+
338
+ - WSL 2:
339
+
340
+ .. code-block:: shell-session
341
+
342
+ $ uname -r
343
+ 5.10.102.1-microsoft-standard-WSL2
344
+ """
345
+ return "Microsoft" in platform.release()
346
+
347
+
348
+ @cache
349
+ def is_wsl2() -> bool:
350
+ """Return `True` only if current platform is Windows Subsystem for Linux v2."""
351
+ return "microsoft" in platform.release()
352
+
353
+
354
+ @cache
355
+ def is_xenserver() -> bool:
356
+ """Return `True` only if current platform is XenServer."""
357
+ return distro.id() == "xenserver"
358
+
359
+
360
+ def recursive_update(
361
+ a: dict[str, Any], b: dict[str, Any], strict: bool = False
362
+ ) -> dict[str, Any]:
363
+ """Like standard ``dict.update()``, but recursive so sub-dict gets updated.
364
+
365
+ Ignore elements present in ``b`` but not in ``a``. Unless ``strict`` is set to
366
+ `True`, in which case a `ValueError` exception will be raised.
367
+ """
368
+ for k, v in b.items():
369
+ if isinstance(v, dict) and isinstance(a.get(k), dict):
370
+ a[k] = recursive_update(a[k], v, strict=strict)
371
+ # Ignore elements unregistered in the template structure.
372
+ elif k in a:
373
+ a[k] = b[k]
374
+ elif strict:
375
+ msg = f"Parameter {k!r} found in second dict but not in first."
376
+ raise ValueError(msg)
377
+ return a
378
+
379
+
380
+ def remove_blanks(
381
+ tree: dict,
382
+ remove_none: bool = True,
383
+ remove_dicts: bool = True,
384
+ remove_str: bool = True,
385
+ ) -> dict:
386
+ """Returns a copy of a dict without items whose values blanks.
387
+
388
+ Are considered blanks:
389
+ - `None` values
390
+ - empty strings
391
+ - empty `dict`
392
+
393
+ The removal of each of these class can be skipped by setting ``remove_*``
394
+ parameters.
395
+
396
+ Dictionarries are inspected recursively and their own blank values are removed.
397
+ """
398
+
399
+ def visit(path, key, value) -> bool:
400
+ """Ignore some class of blank values depending on configuration."""
401
+ if remove_none and value is None:
402
+ return False
403
+ if remove_dicts and isinstance(value, dict) and not len(value):
404
+ return False
405
+ if remove_str and isinstance(value, str) and not len(value):
406
+ return False
407
+ return True
408
+
409
+ return remap(tree, visit=visit)
410
+
411
+
412
+ @dataclass(frozen=True)
413
+ class Platform:
414
+ """A platform can identify multiple distributions or OSes with the same
415
+ characteristics.
416
+
417
+ It has a unique ID, a human-readable name, and boolean to flag current platform.
418
+ """
419
+
420
+ id: str
421
+ """Unique ID of the platform."""
422
+
423
+ name: str
424
+ """User-friendly name of the platform."""
425
+
426
+ icon: str = field(repr=False, default="❓")
427
+ """Icon of the platform."""
428
+
429
+ current: bool = field(init=False)
430
+ """`True` if current environment runs on this platform."""
431
+
432
+ def __post_init__(self):
433
+ """Set the ``current`` attribute to identifying the current platform."""
434
+ check_func_id = f"is_{self.id}"
435
+ assert check_func_id in globals()
436
+ object.__setattr__(self, "current", globals()[check_func_id]())
437
+ object.__setattr__(self, "__doc__", f"Identify {self.name}.")
438
+
439
+ def info(self) -> dict[str, str | bool | None | dict[str, str | None]]:
440
+ """Returns all platform attributes we can gather."""
441
+ info = {
442
+ "id": self.id,
443
+ "name": self.name,
444
+ "icon": self.icon,
445
+ "current": self.current,
446
+ # Extra fields from distro.info().
447
+ "distro_id": None,
448
+ "version": None,
449
+ "version_parts": {"major": None, "minor": None, "build_number": None},
450
+ "like": None,
451
+ "codename": None,
452
+ }
453
+ # Get extra info from distro.
454
+ if self.current:
455
+ distro_info = distro.info()
456
+ # Rename distro ID to avoid conflict with our own ID.
457
+ distro_info["distro_id"] = distro_info.pop("id")
458
+ info = recursive_update(info, remove_blanks(distro_info))
459
+ return info
460
+
461
+
462
+ AIX = Platform("aix", "IBM AIX", "➿")
463
+ ALTLINUX = Platform("altlinux", "ALT Linux")
464
+ AMZN = Platform("amzn", "Amazon Linux", "🙂")
465
+ ANDROID = Platform("android", "Android", "🤖")
466
+ ARCH = Platform("arch", "Arch Linux", "🎗️")
467
+ BUILDROOT = Platform("buildroot", "Buildroot")
468
+ CENTOS = Platform("centos", "CentOS", "💠")
469
+ CLOUDLINUX = Platform("cloudlinux", "CloudLinux OS")
470
+ CYGWIN = Platform("cygwin", "Cygwin", "Ͼ")
471
+ DEBIAN = Platform("debian", "Debian", "🌀")
472
+ EXHERBO = Platform("exherbo", "Exherbo Linux")
473
+ FEDORA = Platform("fedora", "Fedora", "🎩")
474
+ FREEBSD = Platform("freebsd", "FreeBSD", "😈")
475
+ GENTOO = Platform("gentoo", "Gentoo Linux", "🗜️")
476
+ GUIX = Platform("guix", "Guix System")
477
+ HURD = Platform("hurd", "GNU/Hurd", "🐃")
478
+ IBM_POWERKVM = Platform("ibm_powerkvm", "IBM PowerKVM")
479
+ KVMIBM = Platform("kvmibm", "KVM for IBM z Systems")
480
+ LINUXMINT = Platform("linuxmint", "Linux Mint", "🌿")
481
+ MACOS = Platform("macos", "macOS", "🍎")
482
+ MAGEIA = Platform("mageia", "Mageia")
483
+ MANDRIVA = Platform("mandriva", "Mandriva Linux")
484
+ MIDNIGHTBSD = Platform("midnightbsd", "MidnightBSD", "🌘")
485
+ NETBSD = Platform("netbsd", "NetBSD", "🚩")
486
+ OPENBSD = Platform("openbsd", "OpenBSD", "🐡")
487
+ OPENSUSE = Platform("opensuse", "openSUSE", "🦎")
488
+ ORACLE = Platform("oracle", "Oracle Linux", "🦴")
489
+ PARALLELS = Platform("parallels", "Parallels")
490
+ PIDORA = Platform("pidora", "Pidora")
491
+ RASPBIAN = Platform("raspbian", "Raspbian", "🍓")
492
+ RHEL = Platform("rhel", "RedHat Enterprise Linux", "🎩")
493
+ ROCKY = Platform("rocky", "Rocky Linux", "💠")
494
+ SCIENTIFIC = Platform("scientific", "Scientific Linux")
495
+ SLACKWARE = Platform("slackware", "Slackware")
496
+ SLES = Platform("sles", "SUSE Linux Enterprise Server", "🦎")
497
+ SOLARIS = Platform("solaris", "Solaris", "🌞")
498
+ SUNOS = Platform("sunos", "SunOS", "☀️")
499
+ UBUNTU = Platform("ubuntu", "Ubuntu", "🎯")
500
+ UNKNOWN_LINUX = Platform("unknown_linux", "Unknown Linux", "🐧")
501
+ WINDOWS = Platform("windows", "Windows", "🪟")
502
+ WSL1 = Platform("wsl1", "Windows Subsystem for Linux v1", "⊞")
503
+ WSL2 = Platform("wsl2", "Windows Subsystem for Linux v2", "⊞")
504
+ XENSERVER = Platform("xenserver", "XenServer")
505
+
506
+
507
+ @dataclass(frozen=True)
508
+ class Group:
509
+ """A ``Group`` identify a collection of ``Platform``.
510
+
511
+ Used to group platforms of the same family.
512
+ """
513
+
514
+ id: str
515
+ """Unique ID of the group."""
516
+
517
+ name: str
518
+ """User-friendly description of a group."""
519
+
520
+ icon: str = field(repr=False, default="❓")
521
+ """Icon of the group."""
522
+
523
+ platforms: tuple[Platform, ...] = field(repr=False, default_factory=tuple)
524
+ """Sorted list of platforms that belong to this group."""
525
+
526
+ platform_ids: frozenset[str] = field(default_factory=frozenset)
527
+ """Set of platform IDs that belong to this group.
528
+
529
+ Used to test platform overlaps between groups.
530
+ """
531
+
532
+ def __post_init__(self):
533
+ """Keep the platforms sorted by IDs."""
534
+ object.__setattr__(
535
+ self,
536
+ "platforms",
537
+ tuple(sorted(self.platforms, key=lambda p: p.id)),
538
+ )
539
+ object.__setattr__(
540
+ self,
541
+ "platform_ids",
542
+ frozenset({p.id for p in self.platforms}),
543
+ )
544
+ # Double-check there is no duplicate platforms.
545
+ assert len(self.platforms) == len(self.platform_ids)
546
+
547
+ def __iter__(self) -> Iterator[Platform]:
548
+ """Iterate over the platforms of the group."""
549
+ yield from self.platforms
550
+
551
+ def __len__(self) -> int:
552
+ """Return the number of platforms in the group."""
553
+ return len(self.platforms)
554
+
555
+ @staticmethod
556
+ def _extract_platform_ids(other: Group | Iterable[Platform]) -> frozenset[str]:
557
+ """Extract the platform IDs from ``other``."""
558
+ if isinstance(other, Group):
559
+ return other.platform_ids
560
+ return frozenset(p.id for p in other)
561
+
562
+ def isdisjoint(self, other: Group | Iterable[Platform]) -> bool:
563
+ """Return `True` if the group has no platforms in common with ``other``."""
564
+ return self.platform_ids.isdisjoint(self._extract_platform_ids(other))
565
+
566
+ def fullyintersects(self, other: Group | Iterable[Platform]) -> bool:
567
+ """Return `True` if the group has all platforms in common with ``other``.
568
+
569
+ We cannot just compare ``Groups`` with the ``==`` equality operator as the
570
+ latter takes all attributes into account, as per ``dataclass`` default behavior.
571
+ """
572
+ return self.platform_ids == self._extract_platform_ids(other)
573
+
574
+ def issubset(self, other: Group | Iterable[Platform]) -> bool:
575
+ return self.platform_ids.issubset(self._extract_platform_ids(other))
576
+
577
+ def issuperset(self, other: Group | Iterable[Platform]) -> bool:
578
+ return self.platform_ids.issuperset(self._extract_platform_ids(other))
579
+
580
+
581
+ ALL_PLATFORMS: Group = Group(
582
+ "all_platforms",
583
+ "Any platforms",
584
+ "🖥️",
585
+ (
586
+ AIX,
587
+ ALTLINUX,
588
+ AMZN,
589
+ ANDROID,
590
+ ARCH,
591
+ BUILDROOT,
592
+ CENTOS,
593
+ CLOUDLINUX,
594
+ CYGWIN,
595
+ DEBIAN,
596
+ EXHERBO,
597
+ FEDORA,
598
+ FREEBSD,
599
+ GENTOO,
600
+ GUIX,
601
+ HURD,
602
+ IBM_POWERKVM,
603
+ KVMIBM,
604
+ LINUXMINT,
605
+ MACOS,
606
+ MAGEIA,
607
+ MANDRIVA,
608
+ MIDNIGHTBSD,
609
+ NETBSD,
610
+ OPENBSD,
611
+ OPENSUSE,
612
+ ORACLE,
613
+ PARALLELS,
614
+ PIDORA,
615
+ RASPBIAN,
616
+ RHEL,
617
+ ROCKY,
618
+ SCIENTIFIC,
619
+ SLACKWARE,
620
+ SLES,
621
+ SOLARIS,
622
+ SUNOS,
623
+ UBUNTU,
624
+ UNKNOWN_LINUX,
625
+ WINDOWS,
626
+ WSL1,
627
+ WSL2,
628
+ XENSERVER,
629
+ ),
630
+ )
631
+ """All recognized platforms."""
632
+
633
+
634
+ ALL_WINDOWS = Group("all_windows", "Any Windows", "🪟", (WINDOWS,))
635
+ """All Windows operating systems."""
636
+
637
+
638
+ UNIX = Group(
639
+ "unix",
640
+ "Any Unix",
641
+ "⨷",
642
+ tuple(p for p in ALL_PLATFORMS.platforms if p not in ALL_WINDOWS),
643
+ )
644
+ """All Unix-like operating systems and compatibility layers."""
645
+
646
+
647
+ UNIX_WITHOUT_MACOS = Group(
648
+ "unix_without_macos",
649
+ "Any Unix but macOS",
650
+ "⨂",
651
+ tuple(p for p in UNIX if p is not MACOS),
652
+ )
653
+ """All Unix platforms, without macOS.
654
+
655
+ This is useful to avoid macOS-specific workarounds on Unix platforms.
656
+ """
657
+
658
+
659
+ BSD = Group(
660
+ "bsd", "Any BSD", "🅱️", (FREEBSD, MACOS, MIDNIGHTBSD, NETBSD, OPENBSD, SUNOS)
661
+ )
662
+ """All BSD platforms.
663
+
664
+ .. note::
665
+ Are considered of this family (`according Wikipedia
666
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
667
+
668
+ - `386BSD` (`FreeBSD`, `NetBSD`, `OpenBSD`, `DragonFly BSD`)
669
+ - `NeXTSTEP`
670
+ - `Darwin` (`macOS`, `iOS`, `audioOS`, `iPadOS`, `tvOS`, `watchOS`, `bridgeOS`)
671
+ - `SunOS`
672
+ - `Ultrix`
673
+ """
674
+
675
+
676
+ BSD_WITHOUT_MACOS = Group(
677
+ "bsd_without_macos",
678
+ "Any BSD but macOS",
679
+ "🅱️",
680
+ tuple(p for p in BSD if p is not MACOS),
681
+ )
682
+ """All BSD platforms, without macOS.
683
+
684
+ This is useful to avoid macOS-specific workarounds on BSD platforms.
685
+ """
686
+
687
+
688
+ ALL_LINUX = Group(
689
+ "all_linux",
690
+ "Any Linux",
691
+ "🐧",
692
+ (
693
+ ALTLINUX,
694
+ AMZN,
695
+ ANDROID,
696
+ ARCH,
697
+ BUILDROOT,
698
+ CENTOS,
699
+ CLOUDLINUX,
700
+ DEBIAN,
701
+ EXHERBO,
702
+ FEDORA,
703
+ GENTOO,
704
+ GUIX,
705
+ IBM_POWERKVM,
706
+ KVMIBM,
707
+ LINUXMINT,
708
+ MAGEIA,
709
+ MANDRIVA,
710
+ OPENSUSE,
711
+ ORACLE,
712
+ PARALLELS,
713
+ PIDORA,
714
+ RASPBIAN,
715
+ RHEL,
716
+ ROCKY,
717
+ SCIENTIFIC,
718
+ SLACKWARE,
719
+ SLES,
720
+ UBUNTU,
721
+ UNKNOWN_LINUX,
722
+ XENSERVER,
723
+ ),
724
+ )
725
+ """All Unix platforms based on a Linux kernel.
726
+
727
+ .. note::
728
+ Are considered of this family (`according Wikipedia
729
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
730
+
731
+ - `Android`
732
+ - `ChromeOS`
733
+ - any other distribution
734
+ """
735
+
736
+
737
+ LINUX_LAYERS = Group(
738
+ "linux_layers", "Any Linux compatibility layers", "≚", (WSL1, WSL2)
739
+ )
740
+ """Interfaces that allows Linux binaries to run on a different host system.
741
+
742
+ .. note::
743
+ Are considered of this family (`according Wikipedia
744
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
745
+
746
+ - `Windows Subsystem for Linux`
747
+ """
748
+
749
+
750
+ SYSTEM_V = Group(
751
+ "system_v", "Any Unix derived from AT&T System Five", "Ⅴ", (AIX, SOLARIS)
752
+ )
753
+ """All Unix platforms derived from AT&T System Five.
754
+
755
+ .. note::
756
+ Are considered of this family (`according Wikipedia
757
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
758
+
759
+ - `A/UX`
760
+ - `AIX`
761
+ - `HP-UX`
762
+ - `IRIX`
763
+ - `OpenServer`
764
+ - `Solaris`
765
+ - `OpenSolaris`
766
+ - `Illumos`
767
+ - `Tru64`
768
+ - `UNIX`
769
+ - `UnixWare`
770
+ """
771
+
772
+
773
+ UNIX_LAYERS = Group(
774
+ "unix_layers",
775
+ "Any Unix compatibility layers",
776
+ "≛",
777
+ (CYGWIN,),
778
+ )
779
+ """Interfaces that allows Unix binaries to run on a different host system.
780
+
781
+ .. note::
782
+ Are considered of this family (`according Wikipedia
783
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
784
+
785
+ - `Cygwin`
786
+ - `Darling`
787
+ - `Eunice`
788
+ - `GNV`
789
+ - `Interix`
790
+ - `MachTen`
791
+ - `Microsoft POSIX subsystem`
792
+ - `MKS Toolkit`
793
+ - `PASE`
794
+ - `P.I.P.S.`
795
+ - `PWS/VSE-AF`
796
+ - `UNIX System Services`
797
+ - `UserLAnd Technologies`
798
+ - `Windows Services for UNIX`
799
+ """
800
+
801
+
802
+ OTHER_UNIX = Group(
803
+ "other_unix",
804
+ "Any other Unix",
805
+ "⊎",
806
+ tuple(
807
+ p
808
+ for p in UNIX
809
+ if p
810
+ not in (
811
+ BSD.platforms
812
+ + ALL_LINUX.platforms
813
+ + LINUX_LAYERS.platforms
814
+ + SYSTEM_V.platforms
815
+ + UNIX_LAYERS.platforms
816
+ )
817
+ ),
818
+ )
819
+ """All other Unix platforms.
820
+
821
+ .. note::
822
+ Are considered of this family (`according Wikipedia
823
+ <https://en.wikipedia.org/wiki/Template:Unix>`_):
824
+
825
+ - `Coherent`
826
+ - `GNU/Hurd`
827
+ - `HarmonyOS`
828
+ - `LiteOS`
829
+ - `LynxOS`
830
+ - `Minix`
831
+ - `MOS`
832
+ - `OSF/1`
833
+ - `QNX`
834
+ - `BlackBerry 10`
835
+ - `Research Unix`
836
+ - `SerenityOS`
837
+ """
838
+
839
+
840
+ NON_OVERLAPPING_GROUPS: frozenset[Group] = frozenset(
841
+ (
842
+ ALL_WINDOWS,
843
+ BSD,
844
+ ALL_LINUX,
845
+ LINUX_LAYERS,
846
+ SYSTEM_V,
847
+ UNIX_LAYERS,
848
+ OTHER_UNIX,
849
+ ),
850
+ )
851
+ """Non-overlapping groups."""
852
+
853
+
854
+ EXTRA_GROUPS: frozenset[Group] = frozenset(
855
+ (
856
+ ALL_PLATFORMS,
857
+ UNIX,
858
+ UNIX_WITHOUT_MACOS,
859
+ BSD_WITHOUT_MACOS,
860
+ ),
861
+ )
862
+ """Overlapping groups, defined for convenience."""
863
+
864
+
865
+ ALL_GROUPS: frozenset[Group] = frozenset(NON_OVERLAPPING_GROUPS | EXTRA_GROUPS)
866
+ """All groups."""
867
+
868
+
869
+ ALL_OS_LABELS: frozenset[str] = frozenset(p.name for p in ALL_PLATFORMS.platforms)
870
+ """Sets of all recognized labels."""
871
+
872
+
873
+ def reduce(items: Iterable[Group | Platform]) -> set[Group | Platform]:
874
+ """Reduce a collection of ``Group`` and ``Platform`` to a minimal set.
875
+
876
+ Returns a deduplicated set of ``Group`` and ``Platform`` that covers the same exact
877
+ platforms as the original input, but group as much platforms as possible, to reduce
878
+ the number of items.
879
+
880
+ .. hint::
881
+ Maybe this could be solved with some `Euler diagram
882
+ <https://en.wikipedia.org/wiki/Euler_diagram>`_ algorithms, like those
883
+ implemented in `eule <https://github.com/trouchet/eule>`_.
884
+
885
+ This is being discussed upstream at `trouchet/eule#120
886
+ <https://github.com/trouchet/eule/issues/120>`_.
887
+ """
888
+ # Collect all platforms.
889
+ platforms: set[Platform] = set()
890
+ for item in items:
891
+ if isinstance(item, Group):
892
+ platforms.update(item.platforms)
893
+ else:
894
+ platforms.add(item)
895
+
896
+ # List any group matching the platforms.
897
+ valid_groups: set[Group] = set()
898
+ for group in ALL_GROUPS:
899
+ if group.issubset(platforms):
900
+ valid_groups.add(group)
901
+
902
+ # Test all combination of groups to find the smallest set of groups + platforms.
903
+ min_items: int = 0
904
+ results: list[set[Group | Platform]] = []
905
+ # Serialize group sets for deterministic lookups. Sort them by platform count.
906
+ groups = tuple(sorted(valid_groups, key=len, reverse=True))
907
+ for subset_size in range(1, len(groups) + 1):
908
+ # If we already have a solution that involves less items than the current
909
+ # subset of groups we're going to evaluates, there is no point in continuing.
910
+ if min_items and subset_size > min_items:
911
+ break
912
+
913
+ for group_subset in combinations(groups, subset_size):
914
+ # If any group overlaps another, there is no point in exploring this subset.
915
+ if not all(g[0].isdisjoint(g[1]) for g in combinations(group_subset, 2)):
916
+ continue
917
+
918
+ # Remove all platforms covered by the groups.
919
+ ungrouped_platforms = platforms.copy()
920
+ for group in group_subset:
921
+ ungrouped_platforms.difference_update(group.platforms)
922
+
923
+ # Merge the groups and the remaining platforms.
924
+ reduction = ungrouped_platforms.union(group_subset)
925
+ reduction_size = len(reduction)
926
+
927
+ # Reset the results if we have a new solution that is better than the
928
+ # previous ones.
929
+ if not results or reduction_size < min_items:
930
+ results = [reduction]
931
+ min_items = reduction_size
932
+ # If the solution is as good as the previous one, add it to the results.
933
+ elif reduction_size == min_items:
934
+ results.append(reduction)
935
+
936
+ if len(results) > 1:
937
+ msg = f"Multiple solutions found: {results}"
938
+ raise RuntimeError(msg)
939
+
940
+ # If no reduced solution was found, return the original platforms.
941
+ if not results:
942
+ return platforms # type: ignore[return-value]
943
+
944
+ return results.pop()
945
+
946
+
947
+ @cache
948
+ def current_os() -> Platform:
949
+ """Return the current platform."""
950
+ matching = []
951
+ for p in ALL_PLATFORMS.platforms:
952
+ if p.current:
953
+ matching.append(p)
954
+
955
+ if len(matching) > 1:
956
+ msg = f"Multiple platforms match current OS: {matching}"
957
+ raise RuntimeError(msg)
958
+
959
+ if not matching:
960
+ msg = (
961
+ f"Unrecognized {sys.platform} / "
962
+ f"{platform.platform(aliased=True, terse=True)} platform."
963
+ )
964
+ raise SystemError(msg)
965
+
966
+ assert len(matching) == 1
967
+ return matching.pop()
968
+
969
+
970
+ CURRENT_OS_ID: str = current_os().id
971
+ CURRENT_OS_LABEL: str = current_os().name
972
+ """Constants about the current platform."""
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.1
2
+ Name: extra-platforms
3
+ Version: 1.0.0
4
+ Summary: Detect platforms and group them by family
5
+ Author-email: Kevin Deldycke <kevin@deldycke.com>
6
+ Project-URL: Homepage, https://github.com/kdeldycke/extra-platforms
7
+ Project-URL: Documentation, https://kdeldycke.github.io/extra-platforms
8
+ Project-URL: Repository, https://github.com/kdeldycke/extra-platforms
9
+ Project-URL: Funding, https://github.com/sponsors/kdeldycke
10
+ Project-URL: Issues, https://github.com/kdeldycke/extra-platforms/issues
11
+ Project-URL: Changelog, https://kdeldycke.github.io/extra-platforms/changelog.html
12
+ Keywords: multiplatform,pytest,python
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Console
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
18
+ Classifier: Operating System :: MacOS :: MacOS X
19
+ Classifier: Operating System :: Microsoft :: Windows
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.8
23
+ Classifier: Programming Language :: Python :: 3.9
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: Implementation :: CPython
28
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
+ Classifier: Topic :: Utilities
30
+ Classifier: Typing :: Typed
31
+ Requires-Python: >=3.8
32
+ Description-Content-Type: text/markdown
33
+ Requires-Dist: boltons ~=24.0.0
34
+ Requires-Dist: distro ~=1.9.0
35
+ Provides-Extra: docs
36
+ Requires-Dist: furo ~=2024.8.6 ; extra == 'docs'
37
+ Requires-Dist: myst-parser ~=3.0.0 ; extra == 'docs'
38
+ Requires-Dist: sphinx >=7 ; extra == 'docs'
39
+ Requires-Dist: sphinx-autodoc-typehints >=2 ; extra == 'docs'
40
+ Requires-Dist: sphinx-copybutton ~=0.5.2 ; extra == 'docs'
41
+ Requires-Dist: sphinx-design >=0.5 ; extra == 'docs'
42
+ Requires-Dist: sphinx-issues ~=4.1.0 ; extra == 'docs'
43
+ Requires-Dist: sphinxcontrib-mermaid ~=0.9.2 ; extra == 'docs'
44
+ Requires-Dist: sphinxext-opengraph ~=0.9.0 ; extra == 'docs'
45
+ Requires-Dist: tomli ~=2.0.1 ; (python_version < "3.11") and extra == 'docs'
46
+ Provides-Extra: pytest
47
+ Requires-Dist: pytest >=8 ; extra == 'pytest'
48
+ Provides-Extra: test
49
+ Requires-Dist: coverage[toml] ~=7.6.0 ; extra == 'test'
50
+ Requires-Dist: pytest ~=8.3.1 ; extra == 'test'
51
+ Requires-Dist: pytest-cov ~=5.0.0 ; extra == 'test'
52
+ Requires-Dist: pytest-github-actions-annotate-failures ~=0.2.0 ; extra == 'test'
53
+ Requires-Dist: pytest-randomly ~=3.15.0 ; extra == 'test'
54
+
55
+ # Extra Platforms
56
+
57
+ [![Last release](https://img.shields.io/pypi/v/extra-platforms.svg)](https://pypi.python.org/pypi/extra-platforms)
58
+ [![Python versions](https://img.shields.io/pypi/pyversions/extra-platforms.svg)](https://pypi.python.org/pypi/extra-platforms)
59
+ [![Downloads](https://static.pepy.tech/badge/extra_platforms/month)](https://pepy.tech/project/extra_platforms)
60
+ [![Unittests status](https://github.com/kdeldycke/extra-platforms/actions/workflows/tests.yaml/badge.svg?branch=main)](https://github.com/kdeldycke/extra-platforms/actions/workflows/tests.yaml?query=branch%3Amain)
61
+ [![Coverage status](https://codecov.io/gh/kdeldycke/extra-platforms/branch/main/graph/badge.svg)](https://app.codecov.io/gh/kdeldycke/extra-platforms)
62
+ [![Documentation status](https://github.com/kdeldycke/extra-platforms/actions/workflows/docs.yaml/badge.svg?branch=main)](https://github.com/kdeldycke/extra-platforms/actions/workflows/docs.yaml?query=branch%3Amain)
63
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7116050.svg)](https://doi.org/10.5281/zenodo.7116050)
64
+
65
+ ## What is Extra Platforms?
66
+
67
+ > [!NOTE]
68
+ > TODO
69
+
70
+ ## Example
71
+
72
+ > [!NOTE]
73
+ > TODO
74
+
75
+
76
+ ## Used in
77
+
78
+ Check these projects to get real-life examples of `extra-platforms` usage:
79
+
80
+ - ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%E2%AD%90&style=flat-square) [Click Extra](https://github.com/kdeldycke/click-extra#readme) - Drop-in replacement for Click to make user-friendly and colorful CLI.
81
+
82
+ Feel free to send a PR to add your project in this list if you are relying on Click Extra in any way.
83
+
84
+ ## Development
85
+
86
+ [Development guidelines](https://github.com/kdeldycke/click-extra?tab=readme-ov-file#development) are the same as [parent project Click Extra](https://github.com/kdeldycke/click-extra), from which `extra-platforms` originated.
@@ -0,0 +1,7 @@
1
+ extra_platforms/__init__.py,sha256=IpA6CQJ4VXHabn7qf4bA2pkO1jyJB-Wc0dxwYMGvrjY,1219
2
+ extra_platforms/platforms.py,sha256=78DcCNoTKh0i05xZBZxw8Dif3_nzKbGu8_ge3CyGGMc,26955
3
+ extra_platforms-1.0.0.dist-info/METADATA,sha256=clyQ5zdSMpN3kwiE_oMktQw8FKXSDFrFw66hx_SBajQ,4460
4
+ extra_platforms-1.0.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
5
+ extra_platforms-1.0.0.dist-info/entry_points.txt,sha256=isBLN3Ql7i0xvHQnB1S36QVWepytJD7GvesLxzhPjCg,52
6
+ extra_platforms-1.0.0.dist-info/top_level.txt,sha256=9182Fz_BFq0UF5vXtcQFkH1lQOeMK29bs3SH3e6tjJA,16
7
+ extra_platforms-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ extra-platforms = extra_platforms.pytest
@@ -0,0 +1 @@
1
+ extra_platforms