mirror of
https://github.com/HorlogeSkynet/archey4
synced 2025-07-09 00:00:12 +02:00
Compare commits
29 Commits
v4.7.2
...
a575f3b80a
Author | SHA1 | Date | |
---|---|---|---|
a575f3b80a | |||
b16e8c4186 | |||
ccdafef9e7 | |||
28cd846173 | |||
b8f1f799c4 | |||
44f0299379 | |||
bbb39a1d7e | |||
6fe7f45f71 | |||
4154ac03cd | |||
eba811e1ba | |||
a0ef6e3e32 | |||
f15d7eea90 | |||
2b10c7dc00 | |||
f74dda9241 | |||
c6aa6ced96 | |||
aef1cd0bc8 | |||
a1d0f27c5c | |||
a089b80d87 | |||
aab40469b8 | |||
05dfeab716 | |||
c7feeb6921 | |||
3ae7b20e45 | |||
6fed07786b | |||
794e759020 | |||
b680ca3705 | |||
a919c53116 | |||
754e485ff1 | |||
c108f75579 | |||
5c8954e0e5 |
@ -5,6 +5,7 @@ python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
- "3.9-dev"
|
||||
- "pypy3"
|
||||
|
||||
install:
|
||||
|
35
README.md
35
README.md
@ -3,26 +3,21 @@
|
||||
> Archey is a simple system information tool written in Python
|
||||
|
||||
<p align="center">
|
||||
<!-- TRAVIS CI -->
|
||||
<a href="https://travis-ci.org/HorlogeSkynet/archey4"><img src="https://img.shields.io/travis/HorlogeSkynet/archey4/master.svg?style=for-the-badge"></a>
|
||||
<br />
|
||||
<!-- GITHUB -->
|
||||
<!-- GITHUB & TRAVIS CI -->
|
||||
<a href="https://github.com/HorlogeSkynet/archey4/releases/latest"><img src="https://img.shields.io/github/release/HorlogeSkynet/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://travis-ci.org/HorlogeSkynet/archey4"><img src="https://img.shields.io/travis/HorlogeSkynet/archey4/master.svg?style=for-the-badge"></a>
|
||||
<a href="https://github.com/HorlogeSkynet/archey4/commits/master"><img src="https://img.shields.io/github/last-commit/HorlogeSkynet/archey4.svg?style=for-the-badge"></a>
|
||||
<br />
|
||||
<a href="https://github.com/HorlogeSkynet/archey4/issues"><img src="https://img.shields.io/github/issues/HorlogeSkynet/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://github.com/HorlogeSkynet/archey4/pulls"><img src="https://img.shields.io/github/issues-pr/HorlogeSkynet/archey4.svg?style=for-the-badge"></a>
|
||||
<br />
|
||||
<!-- AUR -->
|
||||
<a href="https://aur.archlinux.org/packages/archey4/"><img src="https://img.shields.io/aur/version/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://aur.archlinux.org/packages/archey4/"><img src="https://img.shields.io/aur/votes/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://aur.archlinux.org/packages/archey4/"><img src="https://img.shields.io/aur/license/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://aur.archlinux.org/packages/archey4/"><img src="https://img.shields.io/aur/votes/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://aur.archlinux.org/packages/archey4/"><img src="https://img.shields.io/aur/last-modified/archey4.svg?style=for-the-badge"></a>
|
||||
<br />
|
||||
<!-- PYPI -->
|
||||
<a href="https://pypi.org/project/archey4/"><img src="https://img.shields.io/pypi/v/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://pypi.org/project/archey4/"><img src="https://img.shields.io/pypi/pyversions/archey4.svg?style=for-the-badge"></a>
|
||||
<a href="https://pypi.org/project/archey4/"><img src="https://img.shields.io/pypi/dm/archey4?style=for-the-badge"></a>
|
||||
<a href="https://pypi.org/project/archey4/"><img src="https://img.shields.io/pypi/wheel/archey4.svg?style=for-the-badge"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@ -146,13 +141,13 @@ sudo mv dist/archey /usr/local/bin/
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
archey
|
||||
archey --help
|
||||
```
|
||||
|
||||
or if you only want to try this out (for instance, from source) :
|
||||
|
||||
```bash
|
||||
python3 -m archey
|
||||
python3 -m archey --help
|
||||
```
|
||||
|
||||
## Configuration (optional)
|
||||
@ -175,6 +170,8 @@ Below, some further explanations of each option available :
|
||||
// If set to `false`, configurations defined afterwards won't be loaded.
|
||||
// Developers running Archey from the original project may keep in there the original `config.json` while having their own external configuration set elsewhere.
|
||||
"allow_overriding": true,
|
||||
// Set to `false` to disable multi-threaded loading of entries.
|
||||
"parallel_loading": true,
|
||||
// If set to `true`, any execution warning or error would be hidden.
|
||||
// It may not apply to configuration parsing warnings.
|
||||
"suppress_warnings": false,
|
||||
@ -182,9 +179,9 @@ Below, some further explanations of each option available :
|
||||
// Set to `false` each entry you want to mask.
|
||||
},
|
||||
"colors_palette": {
|
||||
// Set this option to `true` to display a beautiful colors palette.
|
||||
// `false` by default for backward compatibility with non-Unicode locales.
|
||||
"use_unicode": false,
|
||||
// Leave this option set to `true` to display a beautiful colors palette.
|
||||
// Set it to `false` to allow compatibility with non-Unicode locales.
|
||||
"use_unicode": true,
|
||||
// Set this option to `false` to force Archey to use its own colors palettes.
|
||||
// `true` by default to honor `os-release`'s `ANSI_COLOR` option.
|
||||
"honor_ansi_color": true
|
||||
@ -196,9 +193,9 @@ Below, some further explanations of each option available :
|
||||
// The maximum number of local addresses you want to display.
|
||||
// `false` --> Unlimited.
|
||||
"lan_ip_max_count": 2,
|
||||
// `false` would make Archey displays only IPv4 LAN addresses.
|
||||
// `false` would make Archey display IPv4 LAN addresses only.
|
||||
"lan_ip_v6_support": true,
|
||||
// `false` would make Archey displays only IPv4 WAN addresses.
|
||||
// `false` would make Archey display IPv4 WAN addresses only.
|
||||
"wan_ip_v6_support": true
|
||||
},
|
||||
"gpu": {
|
||||
@ -255,13 +252,9 @@ Any improvement would be appreciated.
|
||||
|
||||
## Notes to users
|
||||
|
||||
* If you run `archey` as root, the script will list the processes running by other users on your system in order to display the **Window Manager** & **Desktop Environment** outputs correctly.
|
||||
|
||||
* During the setup procedure, I advised you to copy this script into the `/usr/local/bin/` folder, you may want to check what it does beforehand.
|
||||
|
||||
* If you experience any trouble during the installation or usage, please do **[open an issue](https://github.com/HorlogeSkynet/archey4/issues/new)**.
|
||||
|
||||
* If you had to adapt the script to make it work on your system, please **[open a pull request](https://github.com/HorlogeSkynet/archey4/pulls)** so as to share your modifications with the rest of the world and participate in this project !
|
||||
* If you had to tweak this project to make it work on your system, please **[open a pull request](https://github.com/HorlogeSkynet/archey4/pulls)** so as to share your modifications with the rest of the world and participate in this project !
|
||||
|
||||
* When looking up your public IP address (**WAN\_IP**), Archey will try at first to run a DNS query for `myip.opendns.com`, against OpenDNS's resolver(s). On error, it would fall back on regular HTTPS request(s) to <https://ident.me> ([server sources](https://github.com/pcarrier/identme)).
|
||||
|
||||
|
13
archey.1
13
archey.1
@ -1,5 +1,5 @@
|
||||
.\" Please, before submitting any change, run:
|
||||
.\" `groff -man -Tascii -z archey4.1`
|
||||
.\" `groff -man -Tascii -z archey.1`
|
||||
|
||||
.TH ARCHEY4 1 "${DATE}" "archey4 ${VERSION}" "Archey4 man page"
|
||||
|
||||
@ -31,13 +31,20 @@ Remain \fImaintained\fR, \fIcommunity-driven\fR and
|
||||
.IP "-h, --help"
|
||||
show help message and exit
|
||||
|
||||
.IP "-v, --version"
|
||||
show program's version number and exit
|
||||
.IP "-c, --config-path PATH"
|
||||
path to a configuration file, or a directory containing a `config.json`
|
||||
|
||||
.IP "-j, --json"
|
||||
output entries data to JSON format, use multiple times to increase
|
||||
indentation
|
||||
|
||||
.IP "-s, --screenshot [FILENAME]"
|
||||
take a screenshot once execution is done, optionally specify a target
|
||||
path
|
||||
|
||||
.IP "-v, --version"
|
||||
show program's version number and exit
|
||||
|
||||
.P
|
||||
Archey will regularly run in terminal text output mode if no argument
|
||||
is passed.
|
||||
|
@ -7,13 +7,17 @@ Logos are stored under the `logos` module.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from enum import Enum
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import ExitStack
|
||||
|
||||
from archey._version import __version__
|
||||
from archey.output import Output
|
||||
from archey.configuration import Configuration
|
||||
from archey.processes import Processes
|
||||
from archey.screenshot import take_screenshot
|
||||
from archey.entries.user import User as e_User
|
||||
from archey.entries.hostname import Hostname as e_Hostname
|
||||
from archey.entries.model import Model as e_Model
|
||||
@ -60,37 +64,83 @@ class Entries(Enum):
|
||||
WAN_IP = e_WanIp
|
||||
|
||||
|
||||
def main():
|
||||
"""Simple entry point"""
|
||||
def args_parsing():
|
||||
"""Simple wrapper to `argparse`"""
|
||||
parser = argparse.ArgumentParser(prog='archey')
|
||||
parser.add_argument(
|
||||
'-c', '--config-path',
|
||||
metavar='PATH',
|
||||
help='path to a configuration file, or a directory containing a `config.json`'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-j', '--json',
|
||||
action='count',
|
||||
help='output entries data to JSON format, use multiple times to increase indentation'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--screenshot',
|
||||
metavar='PATH',
|
||||
nargs='?',
|
||||
const=False,
|
||||
help='take a screenshot once execution is done, optionally specify a target path'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--version',
|
||||
action='version',
|
||||
version=__version__
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Simple entry point"""
|
||||
args = args_parsing()
|
||||
|
||||
# `Processes` is a singleton, let's populate the internal list here.
|
||||
Processes()
|
||||
|
||||
# `Configuration` is a singleton, let's populate the internal object here.
|
||||
configuration = Configuration()
|
||||
configuration = Configuration(config_path=args.config_path)
|
||||
|
||||
# From configuration, gather the entries user-enabled.
|
||||
enabled_entries = [
|
||||
(entry.value, entry.name)
|
||||
for entry in Entries
|
||||
if configuration.get('entries', {}).get(entry.name, True)
|
||||
]
|
||||
|
||||
output = Output(
|
||||
format_to_json=args.json
|
||||
)
|
||||
|
||||
for entry in Entries:
|
||||
if configuration.get('entries', {}).get(entry.name, True):
|
||||
output.add_entry(entry.value(name=entry.name))
|
||||
# We will map this function onto our enabled entries to instantiate them.
|
||||
def _entry_instantiator(entry_tuple):
|
||||
return entry_tuple[0](name=entry_tuple[1])
|
||||
|
||||
# Let's use a context manager stack to manage conditional use of `TheadPoolExecutor`.
|
||||
with ExitStack() as cm_stack:
|
||||
if not configuration.get('parallel_loading'):
|
||||
mapper = map
|
||||
else:
|
||||
# Instantiate a threads pool to load our enabled entries in parallel.
|
||||
# We use threads (and not processes) since most work done by our entries is IO-bound.
|
||||
# `max_workers` is manually computed to mimic Python 3.8+ behaviour, but for our needs.
|
||||
# See <https://github.com/python/cpython/pull/13618>.
|
||||
executor = cm_stack.enter_context(ThreadPoolExecutor(
|
||||
max_workers=min(len(enabled_entries) or 1, (os.cpu_count() or 1) + 4)
|
||||
))
|
||||
mapper = executor.map
|
||||
|
||||
for entry_instance in mapper(_entry_instantiator, enabled_entries):
|
||||
output.add_entry(entry_instance)
|
||||
|
||||
output.output()
|
||||
|
||||
# Has the screenshot flag been specified ?
|
||||
if args.screenshot is not None:
|
||||
# If so, but still _falsy_, pass `None` as no output file has been specified by the user.
|
||||
take_screenshot((args.screenshot or None))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""Simple module storing the current project version"""
|
||||
|
||||
__version__ = 'v4.7.2'
|
||||
__version__ = 'v4.8.0-beta'
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"allow_overriding": true,
|
||||
"parallel_loading": true,
|
||||
"suppress_warnings": false,
|
||||
"entries": {
|
||||
"User": true,
|
||||
@ -22,7 +23,7 @@
|
||||
"WAN_IP": true
|
||||
},
|
||||
"colors_palette": {
|
||||
"use_unicode": false,
|
||||
"use_unicode": true,
|
||||
"honor_ansi_color": true
|
||||
},
|
||||
"default_strings": {
|
||||
|
@ -10,13 +10,18 @@ from archey.singleton import Singleton
|
||||
class Configuration(metaclass=Singleton):
|
||||
"""
|
||||
The default needed configuration which will be used by Archey is present below.
|
||||
Values present in the `self.config` dictionary below are needed.
|
||||
New optional values may be added with `_update_recursive()` method.
|
||||
Values present in the `self._config` dictionary below are needed.
|
||||
New optional values may be added with `_update_recursive` method.
|
||||
|
||||
If a `config_path` is passed during instantiation, it will be loaded.
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, config_path=None):
|
||||
self._config = {
|
||||
'allow_overriding': True,
|
||||
'parallel_loading': True,
|
||||
'suppress_warnings': False,
|
||||
'colors_palette': {
|
||||
'use_unicode': False,
|
||||
'use_unicode': True,
|
||||
'honor_ansi_color': True
|
||||
},
|
||||
'default_strings': {
|
||||
@ -57,10 +62,14 @@ class Configuration(metaclass=Singleton):
|
||||
# Let's "save" `STDERR` file descriptor for `suppress_warnings` option
|
||||
self._stderr = sys.stderr
|
||||
|
||||
# Now, let's load each optional configuration file in a "regular" order
|
||||
self.load_configuration('/etc/archey4/')
|
||||
self.load_configuration(os.path.expanduser('~/.config/archey4/'))
|
||||
self.load_configuration(os.path.dirname(os.path.realpath(__file__)))
|
||||
# If a `config_path` has been specified, (try to) load it directly.
|
||||
if config_path:
|
||||
self._load_configuration(config_path)
|
||||
# If not, load each (optional) configuration file in a "regular" order.
|
||||
else:
|
||||
self._load_configuration('/etc/archey4/')
|
||||
self._load_configuration(os.path.expanduser('~/.config/archey4/'))
|
||||
self._load_configuration(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""
|
||||
@ -68,59 +77,57 @@ class Configuration(metaclass=Singleton):
|
||||
"""
|
||||
return self._config.get(key, default)
|
||||
|
||||
def load_configuration(self, path):
|
||||
def _load_configuration(self, path):
|
||||
"""
|
||||
A method handling configuration loading from a JSON file.
|
||||
It will try to load any `config.json` present under `path`.
|
||||
"""
|
||||
# If a previous configuration file has denied overriding...
|
||||
if not self._config.get('allow_overriding', True):
|
||||
if not self.get('allow_overriding'):
|
||||
# ... don't load this one.
|
||||
return
|
||||
|
||||
path = os.path.join(path, 'config.json')
|
||||
# If the specified `path` is a directory, append the file name we are looking for.
|
||||
if os.path.isdir(path):
|
||||
path = os.path.join(path, 'config.json')
|
||||
|
||||
try:
|
||||
with open(path) as file:
|
||||
self._update_recursive(self._config, json.load(file))
|
||||
|
||||
# If the user does not want any warning to appear : 2> /dev/null
|
||||
if self._config.get('suppress_warnings', False):
|
||||
# One more if statement to avoid multiple `open` calls.
|
||||
if sys.stderr == self._stderr:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
|
||||
else:
|
||||
# One more if statement to avoid useless assignments and...
|
||||
# ... for closing previously opened new file descriptor.
|
||||
if sys.stderr != self._stderr:
|
||||
sys.stderr.close()
|
||||
sys.stderr = self._stderr
|
||||
|
||||
with open(path) as f_config:
|
||||
self._update_recursive(self._config, json.load(f_config))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return
|
||||
# For backward compatibility with Python versions prior to 3.5.0
|
||||
# we use `ValueError` instead of `json.JSONDecodeError`.
|
||||
except ValueError as error:
|
||||
print('Warning: {0} ({1})'.format(error, path), file=sys.stderr)
|
||||
except ValueError as value_error:
|
||||
print('Warning: {0} ({1})'.format(value_error, path), file=sys.stderr)
|
||||
return
|
||||
|
||||
# If the user does not want any warning to appear : 2> /dev/null
|
||||
if self.get('suppress_warnings'):
|
||||
# One more if statement to avoid multiple `open` calls.
|
||||
if sys.stderr == self._stderr:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
else:
|
||||
self._close_and_restore_sys_stderr()
|
||||
|
||||
def _update_recursive(self, old_dict, new_dict):
|
||||
"""
|
||||
A method for recursively merging dictionaries as...
|
||||
... `dict.update()` is not able to do this.
|
||||
Original snippet taken from here :
|
||||
https://gist.github.com/angstwad/bf22d1822c38a92ec0a9
|
||||
A method for recursively merging dictionaries as `dict.update()` is not able to do this.
|
||||
Original snippet taken from here : <https://gist.github.com/angstwad/bf22d1822c38a92ec0a9>
|
||||
"""
|
||||
for key, value in new_dict.items():
|
||||
if key in old_dict and isinstance(old_dict[key], dict) \
|
||||
and isinstance(value, dict):
|
||||
if key in old_dict \
|
||||
and isinstance(old_dict[key], dict) \
|
||||
and isinstance(value, dict):
|
||||
self._update_recursive(old_dict[key], value)
|
||||
|
||||
else:
|
||||
old_dict[key] = value
|
||||
|
||||
def __del__(self):
|
||||
def _close_and_restore_sys_stderr(self):
|
||||
"""If modified, close current and restore `sys.stderr` to its original file descriptor"""
|
||||
if sys.stderr != self._stderr:
|
||||
sys.stderr.close()
|
||||
sys.stderr = self._stderr
|
||||
|
||||
def __del__(self):
|
||||
self._close_and_restore_sys_stderr()
|
||||
|
@ -1,6 +1,15 @@
|
||||
"""Distributions enumeration"""
|
||||
"""
|
||||
Distributions enumeration module.
|
||||
Operating Systems detection logic.
|
||||
Interface to `os-release` (through `distro` module).
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
from subprocess import check_output
|
||||
|
||||
import distro
|
||||
|
||||
|
||||
class Distributions(Enum):
|
||||
@ -27,3 +36,46 @@ class Distributions(Enum):
|
||||
SLACKWARE = 'slackware'
|
||||
UBUNTU = 'ubuntu'
|
||||
WINDOWS = 'windows'
|
||||
|
||||
|
||||
@staticmethod
|
||||
def run_detection():
|
||||
"""Entry point of Archey distribution detection logic"""
|
||||
# Are we running on Windows ?
|
||||
if sys.platform in ('win32', 'cygwin'):
|
||||
return Distributions.WINDOWS
|
||||
|
||||
# Is it a Windows Sub-system Linux (WSL) distribution ?
|
||||
# If so, kernel release identifier should keep a trace of it.
|
||||
if b'microsoft' in check_output(['uname', '-r']).lower():
|
||||
return Distributions.WINDOWS
|
||||
|
||||
# Is `ID` (from `os-release`) well-known and supported ?
|
||||
try:
|
||||
return Distributions(distro.id())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Is any of `ID_LIKE` (from `os-release`) well-known and supported ?
|
||||
# See <https://www.freedesktop.org/software/systemd/man/os-release.html#ID_LIKE=>.
|
||||
for id_like in distro.like().split(' '):
|
||||
try:
|
||||
return Distributions(id_like)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# At the moment, fall-back to default `Linux` if nothing of the above matched.
|
||||
return Distributions.LINUX
|
||||
|
||||
@staticmethod
|
||||
def get_distro_name():
|
||||
"""Simple wrapper to `distro` to return the current distribution _pretty_ name"""
|
||||
return distro.name(pretty=True) or None
|
||||
|
||||
@staticmethod
|
||||
def get_ansi_color():
|
||||
"""
|
||||
Simple wrapper to `distro` to return the distribution preferred ANSI color.
|
||||
See <https://www.freedesktop.org/software/systemd/man/os-release.html#ANSI_COLOR=>.
|
||||
"""
|
||||
return distro.os_release_attr('ansi_color') or None
|
||||
|
@ -12,28 +12,37 @@ class CPU(Entry):
|
||||
Parse `/proc/cpuinfo` file to retrieve the model name.
|
||||
If no information could be retrieved, calls `lscpu`.
|
||||
"""
|
||||
_MODEL_NAME_REGEXP = re.compile(
|
||||
r'^model name\s*:\s*(.*)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
model_name_regex = re.compile(
|
||||
r'^model name\s*:\s*(.*)$',
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
with open('/proc/cpuinfo') as file:
|
||||
cpuinfo = re.search(model_name_regex, file.read())
|
||||
|
||||
# This test case has been built for some ARM architectures (see #29).
|
||||
# Sometimes, `model name` info is not present within `/proc/cpuinfo`.
|
||||
# We use the output of `lscpu` program (util-linux-ng) to retrieve it.
|
||||
if not cpuinfo:
|
||||
cpuinfo = re.search(
|
||||
model_name_regex,
|
||||
check_output(
|
||||
['lscpu'],
|
||||
env={'LANG': 'C'}, universal_newlines=True
|
||||
)
|
||||
)
|
||||
cpuinfo_match = self._read_proc_cpuinfo()
|
||||
if not cpuinfo_match:
|
||||
# This test case has been built for some ARM architectures (see #29).
|
||||
# Sometimes, `model name` info is not present within `/proc/cpuinfo`.
|
||||
# We use the output of `lscpu` program (util-linux-ng) to retrieve it.
|
||||
cpuinfo_match = self._run_lscpu()
|
||||
|
||||
# Sometimes CPU model name contains extra ugly white-spaces.
|
||||
self.value = re.sub(r'\s+', ' ', cpuinfo.group(1))
|
||||
self.value = re.sub(r'\s+', ' ', cpuinfo_match.group(1))
|
||||
|
||||
def _read_proc_cpuinfo(self):
|
||||
"""Read `/proc/cpuinfo` and search for our model name pattern"""
|
||||
try:
|
||||
with open('/proc/cpuinfo') as f_cpu_info:
|
||||
return self._MODEL_NAME_REGEXP.search(f_cpu_info.read())
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def _run_lscpu(self):
|
||||
"""Same operation but from `lscpu` output"""
|
||||
return self._MODEL_NAME_REGEXP.search(
|
||||
check_output(
|
||||
['lscpu'],
|
||||
env={'LANG': 'C'}, universal_newlines=True
|
||||
)
|
||||
)
|
||||
|
@ -2,8 +2,7 @@
|
||||
|
||||
from subprocess import check_output
|
||||
|
||||
import distro
|
||||
|
||||
from archey.distributions import Distributions
|
||||
from archey.entry import Entry
|
||||
|
||||
|
||||
@ -12,17 +11,13 @@ class Distro(Entry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
distro_name = distro.name(pretty=True)
|
||||
if not distro_name:
|
||||
distro_name = None
|
||||
|
||||
architecture = check_output(
|
||||
['uname', '-m'],
|
||||
universal_newlines=True
|
||||
).rstrip()
|
||||
|
||||
self.value = {
|
||||
'name': distro_name,
|
||||
'name': Distributions.get_distro_name(),
|
||||
'arch': architecture
|
||||
}
|
||||
|
||||
|
@ -86,8 +86,11 @@ class Model(Entry):
|
||||
|
||||
def _check_rasperry_pi(self):
|
||||
"""Tries to retrieve 'Hardware' and 'Revision IDs' from `/proc/cpuinfo`"""
|
||||
with open('/proc/cpuinfo') as f_cpu_info:
|
||||
cpu_info = f_cpu_info.read()
|
||||
try:
|
||||
with open('/proc/cpuinfo') as f_cpu_info:
|
||||
cpu_info = f_cpu_info.read()
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return
|
||||
|
||||
# If the output contains 'Hardware' and 'Revision'...
|
||||
hardware = re.search('(?<=Hardware\t: ).*', cpu_info)
|
||||
|
@ -16,8 +16,24 @@ class RAM(Entry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
used, total = self._run_free_dash_m()
|
||||
if not total:
|
||||
used, total = self._read_proc_meminfo()
|
||||
|
||||
if not total:
|
||||
return
|
||||
|
||||
self.value = {
|
||||
'used': used,
|
||||
'total': total,
|
||||
'unit': 'MiB'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _run_free_dash_m():
|
||||
"""Call `free -m` and parse its output to retrieve current used and total RAM"""
|
||||
try:
|
||||
ram = ''.join(
|
||||
memory_usage = ''.join(
|
||||
filter(
|
||||
re.compile('Mem').search,
|
||||
check_output(
|
||||
@ -26,37 +42,50 @@ class RAM(Entry):
|
||||
).splitlines()
|
||||
)
|
||||
).split()
|
||||
used = float(ram[2])
|
||||
total = float(ram[1])
|
||||
except (IndexError, FileNotFoundError):
|
||||
# An in-digest one-liner to retrieve memory info into a dictionary
|
||||
with open('/proc/meminfo') as file:
|
||||
ram = {
|
||||
i.split(':')[0]: float(i.split(':')[1].strip(' kB')) / 1024
|
||||
for i in filter(None, file.read().splitlines())
|
||||
}
|
||||
return None, None
|
||||
|
||||
total = ram['MemTotal']
|
||||
# Here, let's imitate Neofetch's behavior.
|
||||
# See <https://github.com/dylanaraps/neofetch/wiki/Frequently-Asked-Questions>.
|
||||
used = total + ram['Shmem'] - (
|
||||
ram['MemFree'] + ram['Cached'] + ram['SReclaimable'] + ram['Buffers'])
|
||||
# Imitates what `free` does when the obtained value happens to be incorrect.
|
||||
# See <https://gitlab.com/procps-ng/procps/blob/master/proc/sysinfo.c#L790>.
|
||||
if used < 0:
|
||||
used = total - ram['MemFree']
|
||||
return float(memory_usage[2]), float(memory_usage[1])
|
||||
|
||||
self.value = {
|
||||
'used': used,
|
||||
'total': total,
|
||||
'unit': 'MiB'
|
||||
}
|
||||
@staticmethod
|
||||
def _read_proc_meminfo():
|
||||
"""Same behavior but by reading from `/proc/meminfo` directly"""
|
||||
try:
|
||||
with open('/proc/meminfo') as f_mem_info:
|
||||
mem_info_lines = f_mem_info.read().splitlines()
|
||||
except (PermissionError, FileNotFoundError):
|
||||
return None, None
|
||||
|
||||
# Store memory information into a dictionary.
|
||||
mem_info = {}
|
||||
for line in filter(None, mem_info_lines):
|
||||
key, value = line.split(':', maxsplit=1)
|
||||
mem_info[key] = float(value.strip(' kB')) / 1024
|
||||
|
||||
total = mem_info['MemTotal']
|
||||
# Here, let's imitate Neofetch's behavior.
|
||||
# See <https://github.com/dylanaraps/neofetch/wiki/Frequently-Asked-Questions>.
|
||||
used = total + mem_info['Shmem'] - (
|
||||
mem_info['MemFree'] + mem_info['Cached']
|
||||
+ mem_info['SReclaimable'] + mem_info['Buffers']
|
||||
)
|
||||
# Imitates what `free` does when the obtained value happens to be incorrect.
|
||||
# See <https://gitlab.com/procps-ng/procps/blob/master/proc/sysinfo.c#L790>.
|
||||
if used < 0:
|
||||
used = total - mem_info['MemFree']
|
||||
|
||||
return used, total
|
||||
|
||||
|
||||
def output(self, output):
|
||||
"""
|
||||
Adds the entry to `output` after pretty-formatting the RAM usage with colour and units.
|
||||
Adds the entry to `output` after pretty-formatting the RAM usage with color and units.
|
||||
"""
|
||||
if not self.value:
|
||||
# Fall back on the default behavior if no RAM usage could be detected.
|
||||
super().output(output)
|
||||
return
|
||||
|
||||
# DRY some constants
|
||||
used = self.value['used']
|
||||
total = self.value['total']
|
||||
|
@ -67,7 +67,6 @@ class Terminal(Entry):
|
||||
"""Build and return a 8-color palette, with Unicode characters if allowed"""
|
||||
# On systems with non-Unicode locales, we imitate '\u2588' character
|
||||
# ... with '#' to display the terminal colors palette.
|
||||
# This is the default option for backward compatibility.
|
||||
use_unicode = self._configuration.get('colors_palette')['use_unicode']
|
||||
|
||||
return ' '.join([
|
||||
|
@ -4,16 +4,11 @@ It supports entries lazy-insertion, logo detection, and final printing.
|
||||
"""
|
||||
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from subprocess import check_output
|
||||
|
||||
from shutil import get_terminal_size
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import distro
|
||||
|
||||
from archey.api import API
|
||||
from archey.colors import ANSI_ECMA_REGEXP, Colors
|
||||
from archey.constants import COLOR_DICT, LOGOS_DICT
|
||||
@ -31,30 +26,14 @@ class Output:
|
||||
# Fetches passed arguments.
|
||||
self._format_to_json = kwargs.get('format_to_json')
|
||||
|
||||
# First we check whether the Kernel has been compiled as a WSL.
|
||||
if 'microsoft' in check_output(['uname', '-r'], universal_newlines=True).lower():
|
||||
self._distribution = Distributions.WINDOWS
|
||||
else:
|
||||
try:
|
||||
self._distribution = Distributions(distro.id())
|
||||
except ValueError:
|
||||
# See <https://www.freedesktop.org/software/systemd/man/os-release.html#ID_LIKE=>.
|
||||
for distro_like in distro.like().split(' '):
|
||||
try:
|
||||
self._distribution = Distributions(distro_like)
|
||||
except ValueError:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
# Well, we didn't match anything so let's fall-back to default `Linux`.
|
||||
self._distribution = Distributions.LINUX
|
||||
# Run distribution detection and fetch a local reference to the
|
||||
self._distribution = Distributions.run_detection()
|
||||
|
||||
# Fetch the colors palette related to this distribution.
|
||||
self._colors_palette = COLOR_DICT[self._distribution]
|
||||
|
||||
# If `os-release`'s `ANSI_COLOR` option is set, honor it.
|
||||
# See <https://www.freedesktop.org/software/systemd/man/os-release.html#ANSI_COLOR=>.
|
||||
ansi_color = distro.os_release_attr('ansi_color')
|
||||
ansi_color = Distributions.get_ansi_color()
|
||||
if ansi_color and Configuration().get('colors_palette')['honor_ansi_color']:
|
||||
# Replace each Archey integrated colors by `ANSI_COLOR`.
|
||||
self._colors_palette = len(self._colors_palette) * \
|
||||
|
@ -1,9 +1,8 @@
|
||||
"""Simple class (acting as a singleton) to handle processes listing"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from subprocess import CalledProcessError, DEVNULL, check_output
|
||||
from subprocess import check_output
|
||||
|
||||
from archey.singleton import Singleton
|
||||
|
||||
@ -13,19 +12,7 @@ class Processes(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
try:
|
||||
self._processes = check_output(
|
||||
[
|
||||
'ps',
|
||||
(('-u' + str(os.getuid())) if os.getuid() != 0 else '-ax'),
|
||||
'-o', 'comm',
|
||||
'--no-headers'
|
||||
],
|
||||
universal_newlines=True, stderr=DEVNULL
|
||||
).splitlines()
|
||||
except CalledProcessError:
|
||||
# The available `ps` implementation may not support passed parameters (hello BusyBox).
|
||||
# Let's fall-back on a much simpler approach.
|
||||
self._processes = check_output(
|
||||
['ps', '-o', 'comm'],
|
||||
['ps', '-eo', 'comm'],
|
||||
universal_newlines=True
|
||||
).splitlines()[1:]
|
||||
except FileNotFoundError:
|
||||
|
86
archey/screenshot.py
Normal file
86
archey/screenshot.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""Simple module doing its best as taking a screenshot of the current screen"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from contextlib import ExitStack
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from subprocess import CalledProcessError, DEVNULL, check_call
|
||||
|
||||
|
||||
def take_screenshot(output_file=None):
|
||||
"""
|
||||
Simple function trying to take a screenshot using various famous back-end programs.
|
||||
When supported by the found and available back-end, try to honor `output_file`.
|
||||
"""
|
||||
if not output_file or os.path.isdir(output_file):
|
||||
# When a directory is provided, we've to force `output_file` to represent a **file** path.
|
||||
output_file = os.path.join(
|
||||
(output_file or os.getcwd()),
|
||||
datetime.now().strftime('archey4_screenshot_%Y-%m-%d_%H.%M.%S.png')
|
||||
)
|
||||
|
||||
# Some programs don't accept specific filename as parameters.
|
||||
# In such cases, we may provide them a target directory instead.
|
||||
output_dir = os.path.dirname(output_file)
|
||||
|
||||
# Back-end programs that _may_ (?) be available across different platforms.
|
||||
screenshot_tools = {
|
||||
'Flameshot': ['flameshot', 'full', '-p', output_dir],
|
||||
'ImageMagick': ['import', '-window', 'root', output_file],
|
||||
'scrot': ['scrot', '-z', output_file],
|
||||
'Shutter': ['shutter', '-f', '-o', output_file, '-e'],
|
||||
}
|
||||
|
||||
# Extends the original screenshot tools dictionary according to current platform.
|
||||
if sys.platform in ('win32', 'cygwin'):
|
||||
screenshot_tools['SnippingTool'] = ['SnippingTool.exe', '/clip']
|
||||
elif sys.platform == 'darwin':
|
||||
screenshot_tools['ScreenCapture'] = [
|
||||
'screencapture',
|
||||
'-x',
|
||||
'-t', output_file.rpart('.')[2],
|
||||
output_file
|
||||
]
|
||||
else: # *NIX systems (and others)...
|
||||
screenshot_tools['GNOME-Screenshot'] = ['gnome-screenshot', '-f', output_file]
|
||||
screenshot_tools['grim'] = ['grim', output_file]
|
||||
screenshot_tools['KDE-Spectacle'] = ['spectacle', '-b', '-o', output_file]
|
||||
screenshot_tools['Xfce4-Screenshooter'] = ['xfce4-screenshooter', '-f', '-s', output_dir]
|
||||
|
||||
# This part purposefully blocks so we wait a little bit before taking the screenshot.
|
||||
# It prevents taking a screenshot before Archey's output has appeared.
|
||||
taking_sc_fstring = '\rTaking screenshot in {:1d}...'
|
||||
for time_remaining in range(3, 0, -1):
|
||||
print(taking_sc_fstring.format(time_remaining), end='', flush=True)
|
||||
time.sleep(1)
|
||||
print('\r' + ' ' * len(taking_sc_fstring), end='\r', flush=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
with ExitStack() as defer_stack:
|
||||
for screenshot_tool, screenshot_cmd in screenshot_tools.items():
|
||||
try:
|
||||
check_call(screenshot_cmd, stderr=DEVNULL)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except CalledProcessError as process_error:
|
||||
defer_stack.callback(partial(
|
||||
print,
|
||||
'Couldn\'t take a screenshot with {}: \"{}\".'.format(
|
||||
screenshot_tool, process_error
|
||||
),
|
||||
file=sys.stderr
|
||||
))
|
||||
continue
|
||||
break
|
||||
else:
|
||||
defer_stack.callback(partial(
|
||||
print,
|
||||
"""\
|
||||
Sorry, we couldn\'t find any supported program to take a screenshot on your system.
|
||||
Please install one of the following and try again: {}.\
|
||||
""".format(', '.join(screenshot_tools.keys())),
|
||||
file=sys.stderr
|
||||
))
|
@ -4,60 +4,70 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from archey.configuration import Configuration
|
||||
|
||||
|
||||
# To avoid edge-case issues due to singleton, we automatically reset internal `_instances`.
|
||||
# This is done at the class-level.
|
||||
@patch.dict(
|
||||
'archey.singleton.Singleton._instances',
|
||||
clear=True
|
||||
)
|
||||
class TestConfigurationUtil(unittest.TestCase):
|
||||
"""
|
||||
Simple test cases to check the behavior of `Configuration` tools.
|
||||
We can't use the `patch` method as the dictionary state after
|
||||
the initializations is unknown due to user's configuration files.
|
||||
Simple test cases to check the behavior of `Configuration` singleton utility class.
|
||||
Values will be manually set in the tests below.
|
||||
"""
|
||||
@patch(
|
||||
# `_load_configuration` method is mocked to "ignore" local system configurations.
|
||||
'archey.configuration.Configuration._load_configuration',
|
||||
Mock()
|
||||
)
|
||||
def test_get(self):
|
||||
"""Test the `get` binder method to configuration elements"""
|
||||
"""Test the `get` binding method to configuration elements"""
|
||||
configuration = Configuration()
|
||||
configuration._config = { # pylint: disable=protected-access
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
configuration.get('ip_settings')['lan_ip_max_count'],
|
||||
2
|
||||
)
|
||||
self.assertFalse(
|
||||
configuration.get('temperature')['use_fahrenheit']
|
||||
)
|
||||
self.assertTrue(configuration.get('does_not_exist', True))
|
||||
self.assertIsNone(configuration.get('does_not_exist_either'))
|
||||
with patch.dict(
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
}
|
||||
):
|
||||
self.assertEqual(configuration.get('ip_settings')['lan_ip_max_count'], 2)
|
||||
self.assertFalse(configuration.get('temperature')['use_fahrenheit'])
|
||||
self.assertTrue(configuration.get('does_not_exist', True))
|
||||
self.assertIsNone(configuration.get('does_not_exist_either'))
|
||||
|
||||
def test_load_configuration(self):
|
||||
"""Test for configuration loading from file, and overriding flag"""
|
||||
configuration = Configuration()
|
||||
configuration._config = { # pylint: disable=protected-access
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': False,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory(suffix='/') as temp_dir:
|
||||
# We create a fake temporary configuration file
|
||||
with open(temp_dir + 'config.json', 'w') as file:
|
||||
file.write("""\
|
||||
with patch.dict(
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': False,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
},
|
||||
clear=True
|
||||
), \
|
||||
tempfile.TemporaryDirectory() as temp_dir:
|
||||
# We create a fake temporary configuration file.
|
||||
with open(os.path.join(temp_dir, 'config.json'), 'w') as f_config:
|
||||
f_config.write("""\
|
||||
{
|
||||
"allow_overriding": false,
|
||||
"suppress_warnings": true,
|
||||
@ -73,8 +83,8 @@ class TestConfigurationUtil(unittest.TestCase):
|
||||
}
|
||||
""")
|
||||
|
||||
# Let's load it into our `Configuration` instance
|
||||
configuration.load_configuration(temp_dir)
|
||||
# Let's load it into our `Configuration` instance.
|
||||
configuration._load_configuration(temp_dir) # pylint: disable=protected-access
|
||||
|
||||
# Let's check the result :S
|
||||
self.assertDictEqual(
|
||||
@ -93,12 +103,14 @@ class TestConfigurationUtil(unittest.TestCase):
|
||||
}
|
||||
}
|
||||
)
|
||||
# The `stderr` file descriptor has changed due to
|
||||
# the `suppress_warnings` option.
|
||||
self.assertNotEqual(configuration._stderr, sys.stderr) # pylint: disable=protected-access
|
||||
# The `stderr` file descriptor has changed due to the `suppress_warnings` option.
|
||||
self.assertNotEqual(
|
||||
configuration._stderr, # pylint: disable=protected-access
|
||||
sys.stderr
|
||||
)
|
||||
|
||||
# Let's try to load the `config.json` file present in this project.
|
||||
configuration.load_configuration(os.getcwd() + '/archey/')
|
||||
configuration._load_configuration('archey/') # pylint: disable=protected-access
|
||||
|
||||
# It should not happen as `allow_overriding` has been set to false.
|
||||
# Thus, the configuration is supposed to be the same as before.
|
||||
@ -122,77 +134,113 @@ class TestConfigurationUtil(unittest.TestCase):
|
||||
def test_update_recursive(self):
|
||||
"""Test for the `_update_recursive` private method"""
|
||||
configuration = Configuration()
|
||||
configuration._config = { # pylint: disable=protected-access
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': False,
|
||||
'default_strings': {
|
||||
'no_address': 'No Address',
|
||||
'not_detected': 'Not detected'
|
||||
},
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
}
|
||||
|
||||
# We change existing values, add new ones, and omit some others.
|
||||
configuration._update_recursive( # pylint: disable=protected-access
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'suppress_warnings': True,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
with patch.dict(
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': False,
|
||||
'default_strings': {
|
||||
'no_address': 'No Address',
|
||||
'not_detected': 'Not detected'
|
||||
},
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False
|
||||
}
|
||||
},
|
||||
'default_strings': {
|
||||
'no_address': '\xde\xad \xbe\xef',
|
||||
'not_detected': 'Not detected',
|
||||
'virtual_environment': 'Virtual Environment'
|
||||
},
|
||||
'temperature': {
|
||||
'a_weird_new_dict': [
|
||||
None,
|
||||
'l33t',
|
||||
{
|
||||
'really': 'one_more_?'
|
||||
}
|
||||
]
|
||||
clear=True
|
||||
):
|
||||
# We change existing values, add new ones, and omit some others.
|
||||
configuration._update_recursive( # pylint: disable=protected-access
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'suppress_warnings': True,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'default_strings': {
|
||||
'no_address': '\xde\xad \xbe\xef',
|
||||
'not_detected': 'Not detected',
|
||||
'virtual_environment': 'Virtual Environment'
|
||||
},
|
||||
'temperature': {
|
||||
'a_weird_new_dict': [
|
||||
None,
|
||||
'l33t',
|
||||
{
|
||||
'really': 'one_more_?'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertDictEqual(
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': True,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'default_strings': {
|
||||
'no_address': '\xde\xad \xbe\xef',
|
||||
'not_detected': 'Not detected',
|
||||
'virtual_environment': 'Virtual Environment'
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False,
|
||||
'a_weird_new_dict': [
|
||||
None,
|
||||
'l33t',
|
||||
{
|
||||
'really': 'one_more_?'
|
||||
}
|
||||
]
|
||||
self.assertDictEqual(
|
||||
configuration._config, # pylint: disable=protected-access
|
||||
{
|
||||
'allow_overriding': True,
|
||||
'suppress_warnings': True,
|
||||
'colors_palette': {
|
||||
'use_unicode': False
|
||||
},
|
||||
'default_strings': {
|
||||
'no_address': '\xde\xad \xbe\xef',
|
||||
'not_detected': 'Not detected',
|
||||
'virtual_environment': 'Virtual Environment'
|
||||
},
|
||||
'ip_settings': {
|
||||
'lan_ip_max_count': 2
|
||||
},
|
||||
'temperature': {
|
||||
'use_fahrenheit': False,
|
||||
'a_weird_new_dict': [
|
||||
None,
|
||||
'l33t',
|
||||
{
|
||||
'really': 'one_more_?'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def test_instantiation_config_path(self):
|
||||
"""Test for configuration loading from specific user-defined path"""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# We create a fake temporary configuration file.
|
||||
config_file = os.path.join(temp_dir, 'user.cfg') # A pure arbitrary name.
|
||||
with open(config_file, 'w') as f_config:
|
||||
f_config.write("""\
|
||||
{
|
||||
"allow_overriding": false,
|
||||
"suppress_warnings": true,
|
||||
"colors_palette": {
|
||||
"use_unicode": false
|
||||
},
|
||||
"ip_settings": {
|
||||
"lan_ip_max_count": 4
|
||||
},
|
||||
"temperature": {
|
||||
"use_fahrenheit": true
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
configuration = Configuration(config_path=config_file)
|
||||
|
||||
# We can't use `assertDictEqual` here as the resulting `_config` internal object
|
||||
# directly depends on the default one (which constantly evolves).
|
||||
# We safely check that above entries have correctly been overridden.
|
||||
self.assertFalse(configuration.get('allow_overriding'))
|
||||
self.assertTrue(configuration.get('suppress_warnings'))
|
||||
self.assertFalse(configuration.get('colors_palette')['use_unicode'])
|
||||
self.assertEqual(configuration.get('ip_settings')['lan_ip_max_count'], 4)
|
||||
self.assertTrue(configuration.get('temperature')['use_fahrenheit'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -77,9 +77,35 @@ model name\t: CPU MODEL\t NAME
|
||||
create=True
|
||||
)
|
||||
def test_spaces_squeezing(self):
|
||||
"""Test name sanitizing, needed on some platformd"""
|
||||
"""Test name sanitizing, needed on some platforms"""
|
||||
self.assertEqual(CPU().value, 'CPU MODEL NAME')
|
||||
|
||||
@patch(
|
||||
'archey.entries.cpu.open',
|
||||
side_effect=PermissionError(),
|
||||
create=True
|
||||
)
|
||||
@patch(
|
||||
'archey.entries.cpu.check_output',
|
||||
return_value="""\
|
||||
Architecture: x86_64
|
||||
CPU op-mode(s): 32-bit, 64-bit
|
||||
Byte Order: Little Endian
|
||||
CPU(s): 4
|
||||
On-line CPU(s) list: 0-3
|
||||
Thread(s) per core: X
|
||||
Core(s) per socket: Y
|
||||
Socket(s): 1
|
||||
NUMA node(s): 1
|
||||
Vendor ID: CPU-VENDOR-NAME
|
||||
CPU family: Z
|
||||
Model: \xde\xad\xbe\xef
|
||||
Model name: CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO
|
||||
""")
|
||||
def test_proc_cpuinfo_unreadable(self, _, __):
|
||||
"""Check Archey does not crash when `/proc/cpuinfo` is not readable"""
|
||||
self.assertEqual(CPU().value, 'CPU-MODEL-NAME-WITHOUT-PROC-CPUINFO')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
153
archey/test/test_archey_distributions.py
Normal file
153
archey/test/test_archey_distributions.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""Test module for `archey.distributions`"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from archey.distributions import Distributions
|
||||
|
||||
|
||||
class TestDistributionsUtil(unittest.TestCase):
|
||||
"""
|
||||
Test cases for the `Distributions` (enumeration / utility) class.
|
||||
"""
|
||||
def test_constant_values(self):
|
||||
"""Test enumeration member instantiation from value"""
|
||||
self.assertEqual(Distributions('debian'), Distributions.DEBIAN)
|
||||
self.assertRaises(ValueError, Distributions, 'unknown')
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'win32'
|
||||
)
|
||||
def test_detection_windows(self):
|
||||
"""Test output for Windows"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.WINDOWS
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-Microsoft\n'
|
||||
)
|
||||
def test_detection_windows_subsystem(self, _):
|
||||
"""Test output for Windows Subsystem Linux"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.WINDOWS
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.id',
|
||||
return_value='debian'
|
||||
)
|
||||
def test_detection_known_distro_id(self, _, __):
|
||||
"""Test known distribution output"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.DEBIAN
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.id',
|
||||
return_value='an-unknown-distro-id'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.like',
|
||||
return_value='' # No `ID_LIKE` specified.
|
||||
)
|
||||
def test_detection_unknown_distro_id(self, _, __, ___):
|
||||
"""Test unknown distribution output"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.LINUX
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.like',
|
||||
return_value='ubuntu' # Oh, it's actually an Ubuntu-based one !
|
||||
)
|
||||
def test_detection_known_distro_like(self, _, __, ___):
|
||||
"""Test distribution matching from the `os-release`'s `ID_LIKE` option"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.UBUNTU
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.like',
|
||||
return_value='an-unknown-distro-id arch' # Hmmm, an unknown Arch-based...
|
||||
)
|
||||
def test_detection_distro_like_second(self, _, __, ___):
|
||||
"""Test distribution matching from the `os-release`'s `ID_LIKE` option (second candidate)"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.ARCH_LINUX
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.distributions.sys.platform',
|
||||
'linux'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.check_output',
|
||||
return_value=b'X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.distributions.distro.like',
|
||||
return_value='' # No `ID_LIKE` either...
|
||||
)
|
||||
def test_detection_both_distro_calls_fail(self, _, __, ___):
|
||||
"""Test distribution fall-back when `distro` soft-fail two times"""
|
||||
self.assertEqual(
|
||||
Distributions.run_detection(),
|
||||
Distributions.LINUX
|
||||
)
|
@ -7,16 +7,16 @@ from archey.entries.distro import Distro
|
||||
|
||||
|
||||
class TestDistroEntry(unittest.TestCase):
|
||||
"""We mock the `distro` vendor module call, as long as the `check_output` one"""
|
||||
@patch(
|
||||
'archey.entries.distro.distro.name', # `distro.name` output
|
||||
return_value="""\
|
||||
NAME VERSION (CODENAME)\
|
||||
""")
|
||||
"""`Distro` entry simple test cases"""
|
||||
@patch(
|
||||
'archey.entries.distro.check_output', # `uname` output
|
||||
return_value="""\
|
||||
ARCHITECTURE
|
||||
""")
|
||||
@patch(
|
||||
'archey.entries.distro.Distributions.get_distro_name',
|
||||
return_value="""\
|
||||
NAME VERSION (CODENAME)\
|
||||
""")
|
||||
def test_ok(self, _, __):
|
||||
"""Test for `distro` and `uname` retrievals"""
|
||||
@ -28,15 +28,15 @@ ARCHITECTURE
|
||||
}
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.entries.distro.distro.name', # `distro.name` output
|
||||
return_value="" # `distro` is soft-failing, returning an empty string...
|
||||
)
|
||||
@patch(
|
||||
'archey.entries.distro.check_output', # `uname` output
|
||||
return_value="""\
|
||||
ARCHITECTURE
|
||||
""")
|
||||
@patch(
|
||||
'archey.entries.distro.Distributions.get_distro_name',
|
||||
return_value=None # Soft-failing : No _pretty_ distribution name found...
|
||||
)
|
||||
@patch(
|
||||
'archey.configuration.Configuration.get',
|
||||
return_value={'not_detected': 'Not detected'}
|
||||
|
@ -260,13 +260,18 @@ class TestLanIpEntry(unittest.TestCase, CustomAssertions):
|
||||
'archey.entries.lan_ip.netifaces',
|
||||
None # Imitate an `ImportError` behavior.
|
||||
)
|
||||
@patch(
|
||||
'archey.entries.lan_ip.print',
|
||||
return_value=None, # Let's nastily mute class' outputs.
|
||||
create=True
|
||||
)
|
||||
@patch(
|
||||
'archey.configuration.Configuration.get',
|
||||
side_effect=[
|
||||
{'not_detected': 'Not detected'}
|
||||
]
|
||||
)
|
||||
def test_netifaces_not_available(self, _):
|
||||
def test_netifaces_not_available(self, _, __):
|
||||
"""Check `netifaces` is really acting as a (soft-)dependency"""
|
||||
lan_ip = LanIp()
|
||||
|
||||
|
@ -15,9 +15,6 @@ class TestModelEntry(unittest.TestCase):
|
||||
* Raspberry Pi
|
||||
* Virtual environment (as a VM or a container)
|
||||
"""
|
||||
def setUp(self):
|
||||
self._return_values = None
|
||||
|
||||
@patch(
|
||||
'archey.entries.model.check_output',
|
||||
side_effect=CalledProcessError(1, 'systemd-detect-virt', "none\n")
|
||||
@ -37,13 +34,12 @@ class TestModelEntry(unittest.TestCase):
|
||||
)
|
||||
def test_raspberry(self, _):
|
||||
"""Test for a typical Raspberry context"""
|
||||
self._return_values = [
|
||||
FileNotFoundError(), # First `open` call will fail
|
||||
'Hardware\t: HARDWARE\nRevision\t: REVISION\n'
|
||||
]
|
||||
|
||||
with patch('archey.entries.model.open', mock_open(), create=True) as mock:
|
||||
mock.return_value.read.side_effect = self._special_func_for_mock_open
|
||||
mock.return_value.read.side_effect = [
|
||||
FileNotFoundError(), # First `open` call will (`/sys/[...]/product_name`)
|
||||
'Hardware\t: HARDWARE\nRevision\t: REVISION\n'
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
Model().value,
|
||||
'Raspberry Pi HARDWARE (Rev. REVISION)'
|
||||
@ -136,13 +132,11 @@ class TestModelEntry(unittest.TestCase):
|
||||
)
|
||||
def test_no_match(self, _, __, ___):
|
||||
"""Test when no information could be retrieved"""
|
||||
self._return_values = [
|
||||
FileNotFoundError(), # First `open` call will fail
|
||||
'Hardware\t: HARDWARE\n' # `Revision` entry is not present
|
||||
]
|
||||
|
||||
with patch('archey.entries.model.open', mock_open(), create=True) as mock:
|
||||
mock.return_value.read.side_effect = self._special_func_for_mock_open
|
||||
mock.return_value.read.side_effect = [
|
||||
FileNotFoundError(), # First `open` call will (`/sys/[...]/product_name`)
|
||||
PermissionError() # `/proc/cpuinfo` won't be available
|
||||
]
|
||||
|
||||
model = Model()
|
||||
|
||||
@ -156,22 +150,5 @@ class TestModelEntry(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def _special_func_for_mock_open(self):
|
||||
"""
|
||||
This method does not belong to the test cases.
|
||||
It's a special method which allows mocking multiple `io.open` calls.
|
||||
You just have to set the values within a list `self._return_values`.
|
||||
And then :
|
||||
`mock.return_value.read.side_effect = self._special_func_for_mock_open`
|
||||
"""
|
||||
return_value = self._return_values.pop(0)
|
||||
|
||||
# Either return value or raise any specified exception
|
||||
if issubclass(return_value.__class__, OSError().__class__):
|
||||
raise return_value
|
||||
|
||||
return return_value
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -13,178 +13,11 @@ from archey.distributions import Distributions
|
||||
|
||||
class TestOutputUtil(unittest.TestCase):
|
||||
"""
|
||||
Simple test cases to check the behavior of `Output` main class.
|
||||
Simple test cases to check the behavior of the `Output` class.
|
||||
"""
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='debian'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_known_distro(self, _, __, ___):
|
||||
"""Test known distribution output"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.DEBIAN
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='an-unknown-distro-id'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.like',
|
||||
return_value='' # No `ID_LIKE` specified.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_unknown_distro(self, _, __, ___, ____):
|
||||
"""Test unknown distribution output"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.LINUX
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.like',
|
||||
return_value='ubuntu' # Oh, it's actually an Ubuntu-based one !
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_distro_like(self, _, __, ___, ____):
|
||||
"""Test distribution matching from the `os-release`'s `ID_LIKE` option"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.UBUNTU
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.like',
|
||||
return_value='linuxmint debian' # Oh, what do we got there ?!
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_distro_like_first(self, _, __, ___, ____):
|
||||
"""Test distribution matching from the `os-release`'s `ID_LIKE` option (multiple entries)"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.LINUX_MINT
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.like',
|
||||
return_value='an-unknown-distro-id arch' # Hmmm, an unknown Arch-based...
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_distro_like_second(self, _, __, ___, ____):
|
||||
"""Test distribution matching from the `os-release`'s `ID_LIKE` option (second candidate)"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.ARCH_LINUX
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='' # Unknown distribution.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.like',
|
||||
return_value='' # No `ID_LIKE` either...
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_both_distro_calls_fail(self, _, __, ___, ____):
|
||||
"""Test distribution fall-back when `distro` soft-fail two times"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.LINUX
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-Microsoft\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
)
|
||||
def test_init_windows_subsystem(self, _, __):
|
||||
"""Test output for Windows Subsystem Linux"""
|
||||
output = Output()
|
||||
|
||||
self.assertEqual(
|
||||
output._distribution, # pylint: disable=protected-access
|
||||
Distributions.WINDOWS
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='debian' # Make Debian being selected.
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.DEBIAN # Make Debian being selected.
|
||||
)
|
||||
@patch.dict(
|
||||
'archey.output.COLOR_DICT',
|
||||
@ -194,7 +27,7 @@ class TestOutputUtil(unittest.TestCase):
|
||||
'archey.output.Configuration.get',
|
||||
return_value={'honor_ansi_color': False}
|
||||
)
|
||||
def test_append_regular(self, _, __, ___):
|
||||
def test_append_regular(self, _, __):
|
||||
"""Test the `append` method, for new entries"""
|
||||
output = Output()
|
||||
output.append('KEY', 'VALUE')
|
||||
@ -205,22 +38,18 @@ class TestOutputUtil(unittest.TestCase):
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.SLACKWARE # Make Slackware being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='slackware' # Make Slackware being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
'archey.output.Distributions.get_ansi_color',
|
||||
return_value='ANSI_COLOR'
|
||||
)
|
||||
@patch(
|
||||
'archey.output.Configuration.get',
|
||||
return_value={'honor_ansi_color': True}
|
||||
)
|
||||
def test_append_ansi_color(self, _, __, ___, ____):
|
||||
def test_append_ansi_color(self, _, __, ___):
|
||||
"""Check that `Output` honor `ANSI_COLOR` as required"""
|
||||
output = Output()
|
||||
|
||||
@ -234,11 +63,11 @@ class TestOutputUtil(unittest.TestCase):
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-Microsoft\n' # Make WSL detection pass.
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.WINDOWS # Make WSL detection pass.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
'archey.output.Distributions.get_ansi_color',
|
||||
return_value='ANSI_COLOR'
|
||||
)
|
||||
@patch(
|
||||
@ -259,16 +88,12 @@ class TestOutputUtil(unittest.TestCase):
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.DEBIAN # Make Debian being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='debian' # Make Debian being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
'archey.output.Distributions.get_ansi_color',
|
||||
return_value=None
|
||||
)
|
||||
@patch.dict(
|
||||
'archey.output.LOGOS_DICT',
|
||||
@ -306,7 +131,7 @@ class TestOutputUtil(unittest.TestCase):
|
||||
return_value=None, # Let's nastily mute class' outputs.
|
||||
create=True
|
||||
)
|
||||
def test_centered_output(self, print_mock, _, __, ___):
|
||||
def test_centered_output(self, print_mock, _, __):
|
||||
"""Test how the `output` method handles centering operations"""
|
||||
output = Output()
|
||||
|
||||
@ -329,7 +154,7 @@ class TestOutputUtil(unittest.TestCase):
|
||||
)
|
||||
|
||||
# Entries bigger than logo
|
||||
output._results = [ # pylint: disable=protected-access
|
||||
output._results = [ # pylint: disable=protected-access
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11',
|
||||
'12', '13', '14', '15', '16', '17', '18', '19', '20', '21'
|
||||
]
|
||||
@ -377,7 +202,7 @@ FAKE_COLOR 21\x1b[0m\
|
||||
)
|
||||
|
||||
# Entries bigger than logo
|
||||
output._results = [ # pylint: disable=protected-access
|
||||
output._results = [ # pylint: disable=protected-access
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12',
|
||||
'13', '14', '15', '16', '17', '18', '19', '20', '21', '22'
|
||||
]
|
||||
@ -436,16 +261,12 @@ FAKE_COLOR 22\x1b[0m\
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.output.check_output',
|
||||
return_value='X.Y.Z-R-ARCH\n'
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.DEBIAN # Make Debian being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.id',
|
||||
return_value='debian' # Select Debian
|
||||
)
|
||||
@patch(
|
||||
'archey.output.distro.os_release_attr',
|
||||
return_value=''
|
||||
'archey.output.Distributions.get_ansi_color',
|
||||
return_value=None
|
||||
)
|
||||
@patch.dict(
|
||||
'archey.output.LOGOS_DICT',
|
||||
@ -465,7 +286,7 @@ FAKE_COLOR 22\x1b[0m\
|
||||
return_value=None, # Let's nastily mute class' outputs.
|
||||
create=True
|
||||
)
|
||||
def test_line_wrapping(self, print_mock, termsize_mock, _, __, ___):
|
||||
def test_line_wrapping(self, print_mock, termsize_mock, _, __):
|
||||
"""Test how the `output` method handles wrapping lines that are too long"""
|
||||
output = Output()
|
||||
|
||||
@ -493,6 +314,10 @@ O \x1b[0;31m\x1b[0m...\x1b[0m\
|
||||
# `unittest.mock.Mock.assert_called_once` is not available against Python < 3.6.
|
||||
self.assertEqual(print_mock.call_count, 1)
|
||||
|
||||
@patch(
|
||||
'archey.output.Distributions.run_detection',
|
||||
return_value=Distributions.DEBIAN # Make Debian being selected.
|
||||
)
|
||||
@patch(
|
||||
'archey.output.Configuration.get',
|
||||
return_value={'honor_ansi_color': False}
|
||||
@ -502,7 +327,7 @@ O \x1b[0;31m\x1b[0m...\x1b[0m\
|
||||
return_value=None, # Let's nastily mute class' outputs.
|
||||
create=True
|
||||
)
|
||||
def test_json_output_format(self, print_mock, _):
|
||||
def test_json_output_format(self, print_mock, _, __):
|
||||
"""Test how the `output` method handles JSON preferred formatting of entries"""
|
||||
output = Output(format_to_json=True)
|
||||
# We can't set the `name` attribute of a mock on its creation,
|
||||
@ -514,7 +339,7 @@ O \x1b[0;31m\x1b[0m...\x1b[0m\
|
||||
mocked_entries[0].name = 'test'
|
||||
mocked_entries[1].name = 'name'
|
||||
|
||||
output._entries = mocked_entries # pylint: disable=protected-access
|
||||
output._entries = mocked_entries # pylint: disable=protected-access
|
||||
output.output()
|
||||
|
||||
# Check that `print` output is properly formatted as JSON, with expected results.
|
||||
|
@ -1,26 +1,27 @@
|
||||
"""Test module for `archey.processes`"""
|
||||
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from archey.processes import Processes
|
||||
|
||||
|
||||
# To avoid edge-case issues due to singleton, we automatically reset internal `_instances`.
|
||||
# This is done at the class-level.
|
||||
@patch.dict(
|
||||
'archey.singleton.Singleton._instances',
|
||||
clear=True
|
||||
)
|
||||
class TestProcessesUtil(unittest.TestCase):
|
||||
"""
|
||||
Test cases for the `Processes` (singleton) class.
|
||||
To work around the singleton, we reset the internal `_instances` dictionary.
|
||||
This way, `check_output` can be mocked here.
|
||||
"""
|
||||
@patch.dict(
|
||||
'archey.singleton.Singleton._instances',
|
||||
clear=True
|
||||
)
|
||||
@patch(
|
||||
'archey.processes.check_output',
|
||||
return_value="""\
|
||||
COMMAND
|
||||
what
|
||||
an
|
||||
awesome
|
||||
@ -45,31 +46,6 @@ there
|
||||
# `unittest.mock.Mock.assert_called_once` is not available against Python < 3.6.
|
||||
self.assertEqual(check_output_mock.call_count, 1)
|
||||
|
||||
@patch.dict(
|
||||
'archey.singleton.Singleton._instances',
|
||||
clear=True
|
||||
)
|
||||
@patch(
|
||||
'archey.processes.check_output',
|
||||
side_effect=[
|
||||
CalledProcessError(1, 'ps', "ps: unrecognized option: u\n"),
|
||||
"""\
|
||||
COMMAND
|
||||
sh
|
||||
top
|
||||
ps
|
||||
"""])
|
||||
def test_ps_failed(self, _):
|
||||
"""Verifies that the program correctly handles first crashing `ps` call"""
|
||||
self.assertListEqual(
|
||||
Processes().get(),
|
||||
['sh', 'top', 'ps']
|
||||
)
|
||||
|
||||
@patch.dict(
|
||||
'archey.singleton.Singleton._instances',
|
||||
clear=True
|
||||
)
|
||||
@patch(
|
||||
'archey.processes.check_output',
|
||||
side_effect=FileNotFoundError()
|
||||
|
@ -142,6 +142,34 @@ SUnreclaim: 113308 kB
|
||||
}
|
||||
)
|
||||
|
||||
@patch(
|
||||
'archey.entries.ram.check_output',
|
||||
side_effect=IndexError() # `free` call will fail
|
||||
)
|
||||
@patch(
|
||||
'archey.entries.ram.open',
|
||||
side_effect=PermissionError(),
|
||||
create=True
|
||||
)
|
||||
@patch(
|
||||
'archey.configuration.Configuration.get',
|
||||
side_effect=[
|
||||
{'not_detected': 'Not detected'}
|
||||
]
|
||||
)
|
||||
def test_not_detected(self, _, __, ___):
|
||||
"""Check Archey does not crash when `/proc/meminfo` is not readable"""
|
||||
ram = RAM()
|
||||
|
||||
output_mock = MagicMock()
|
||||
ram.output(output_mock)
|
||||
|
||||
self.assertIsNone(ram.value)
|
||||
self.assertEqual(
|
||||
output_mock.append.call_args[0][1],
|
||||
'Not detected'
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
@ -335,14 +335,19 @@ class TestTemperatureEntry(unittest.TestCase):
|
||||
)
|
||||
def test_celsius_to_fahrenheit_conversion(self, _, __, ___):
|
||||
"""Simple tests for the `_convert_to_fahrenheit` static method"""
|
||||
temperature = Temperature()
|
||||
# pylint: disable=protected-access
|
||||
self.assertAlmostEqual(temperature._convert_to_fahrenheit(-273.15), -459.67)
|
||||
self.assertAlmostEqual(temperature._convert_to_fahrenheit(0.0), 32.0)
|
||||
self.assertAlmostEqual(temperature._convert_to_fahrenheit(21.0), 69.8)
|
||||
self.assertAlmostEqual(temperature._convert_to_fahrenheit(37.0), 98.6)
|
||||
self.assertAlmostEqual(temperature._convert_to_fahrenheit(100.0), 212.0)
|
||||
# pylint: enable=protected-access
|
||||
test_conversion_cases = [
|
||||
(-273.15, -459.67),
|
||||
(0.0, 32.0),
|
||||
(21.0, 69.8),
|
||||
(37.0, 98.6),
|
||||
(100.0, 212.0)
|
||||
]
|
||||
|
||||
for celsius_value, expected_fahrenheit in test_conversion_cases:
|
||||
self.assertAlmostEqual(
|
||||
Temperature._convert_to_fahrenheit(celsius_value), # pylint: disable=protected-access
|
||||
expected_fahrenheit
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -26,8 +26,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
)
|
||||
def test_terminal_emulator_term_program(self, _):
|
||||
"""Check that `TERM_PROGRAM` is honored even if `TERM` or `COLORTERM` is defined"""
|
||||
output = Terminal().value
|
||||
self.assertTrue(output.startswith('A-COOL-TERMINAL-EMULATOR'))
|
||||
self.assertEqual(Terminal().value, 'A-COOL-TERMINAL-EMULATOR')
|
||||
|
||||
@patch.dict(
|
||||
'archey.entries.terminal.os.environ',
|
||||
@ -43,8 +42,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
)
|
||||
def test_terminal_emulator_special_term(self, _):
|
||||
"""Check that `TERM` is honored even if a "known identifier" could be found"""
|
||||
output = Terminal().value
|
||||
self.assertTrue(output.startswith('OH-A-SPECIAL-CASE'))
|
||||
self.assertEqual(Terminal().value, 'OH-A-SPECIAL-CASE')
|
||||
|
||||
@patch.dict(
|
||||
'archey.entries.terminal.os.environ',
|
||||
@ -60,8 +58,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
)
|
||||
def test_terminal_emulator_name_normalization(self, _):
|
||||
"""Check that our manual terminal detection as long as name normalization are working"""
|
||||
output = Terminal().value
|
||||
self.assertTrue(output.startswith('Konsole'))
|
||||
self.assertEqual(Terminal().value, 'Konsole')
|
||||
|
||||
@patch.dict(
|
||||
'archey.entries.terminal.os.environ',
|
||||
@ -79,7 +76,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
output_mock = MagicMock()
|
||||
terminal.output(output_mock)
|
||||
|
||||
self.assertTrue(terminal.value.startswith('xterm-256color'))
|
||||
self.assertEqual(terminal.value, 'xterm-256color')
|
||||
self.assertTrue(
|
||||
output_mock.append.call_args[0][1].startswith('xterm-256color')
|
||||
)
|
||||
@ -99,8 +96,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
)
|
||||
def test_terminal_emulator_colorterm(self, _):
|
||||
"""Check we can detect terminals using the `COLORTERM` environment variable."""
|
||||
output = Terminal().value
|
||||
self.assertTrue(output.startswith('KMSCON'))
|
||||
self.assertEqual(Terminal().value, 'KMSCON')
|
||||
|
||||
@patch.dict(
|
||||
'archey.entries.terminal.os.environ',
|
||||
@ -119,8 +115,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
"""
|
||||
Check we observe terminal using `COLORTERM` even if `TERM` or a "known identifier" is found.
|
||||
"""
|
||||
output = Terminal().value
|
||||
self.assertTrue(output.startswith('KMSCON'))
|
||||
self.assertEqual(Terminal().value, 'KMSCON')
|
||||
|
||||
@patch.dict(
|
||||
'archey.entries.terminal.os.environ',
|
||||
@ -143,7 +138,7 @@ class TestTerminalEntry(unittest.TestCase):
|
||||
output = output_mock.append.call_args[0][1]
|
||||
|
||||
self.assertIsNone(terminal.value)
|
||||
self.assertTrue(output.startswith('Not detected '))
|
||||
self.assertTrue(output.startswith('Not detected'))
|
||||
self.assertFalse(output.count('\u2588'))
|
||||
|
||||
|
||||
|
@ -7,3 +7,12 @@ set -e
|
||||
if [ -L /usr/bin/archey4 ]; then
|
||||
rm /usr/bin/archey4
|
||||
fi
|
||||
|
||||
|
||||
# Removes any byte-code file that may have been generated by Archey.
|
||||
# Wild-cards are being used to match all supported distribution layouts.
|
||||
find /usr/lib/python3*/*-packages/archey \
|
||||
-type d \
|
||||
-name __pycache__ \
|
||||
-exec \
|
||||
rm -r {} +
|
||||
|
@ -20,10 +20,6 @@
|
||||
# Run it as :
|
||||
# $ bash packaging/build.sh [REVISION] [0xGPG_IDENTITY]
|
||||
#
|
||||
# Known packages error (FPM bug ?) :
|
||||
# * Arch Linux :
|
||||
# * `--pacman-optional-depends` appears to be ignored [jordansissel/fpm#1619]
|
||||
#
|
||||
# If you happen to tweak packaging scripts, please lint them before submitting changes :
|
||||
# $ shellcheck packaging/*
|
||||
#
|
||||
@ -58,12 +54,12 @@ FPM_COMMON_ARGS=(
|
||||
--config-files "etc/archey4/config.json" \
|
||||
--architecture all \
|
||||
--maintainer "${AUTHOR} <${AUTHOR_EMAIL}>" \
|
||||
--after-install packaging/after_install \
|
||||
--after-upgrade packaging/after_install \
|
||||
--before-remove packaging/before_remove \
|
||||
--after-install ./packaging/after_install \
|
||||
--after-upgrade ./packaging/after_install \
|
||||
--before-remove ./packaging/before_remove \
|
||||
--python-bin python3 \
|
||||
--python-install-bin usr/bin/ \
|
||||
--python-install-data usr/ \
|
||||
--python-install-bin 'usr/bin/' \
|
||||
--python-install-data 'usr/' \
|
||||
--no-python-fix-name \
|
||||
--no-python-dependencies \
|
||||
)
|
||||
@ -82,6 +78,10 @@ sed -e "s/\${DATE}/$(date +'%B %Y')/1" archey.1 | \
|
||||
|
||||
|
||||
# Prevent Setuptools from generating byte-code files.
|
||||
# Important note :
|
||||
# It allows the packager to build generic distribution packages without shipping byte-code related to its Python interpreter version.
|
||||
# A noticeable side-effect may be the appearance of "untracked" byte-code files (when running Archey as root for instance).
|
||||
# Check `packaging/before_remove` script to see how Archey deals with them.
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@ fpm \
|
||||
--depends 'python3 >= 3.4' \
|
||||
--depends 'python3-distro' \
|
||||
--depends 'python3-netifaces' \
|
||||
--python-install-lib usr/lib/python3/dist-packages/ \
|
||||
--python-install-lib 'usr/lib/python3/dist-packages/' \
|
||||
--deb-priority 'optional' \
|
||||
--deb-field 'Suggests: dnsutils, lm-sensors, pciutils, wmctrl, virt-what, btrfs-progs' \
|
||||
--deb-no-default-config-files \
|
||||
@ -170,13 +170,13 @@ python3 setup.py -q sdist bdist_wheel
|
||||
|
||||
# Check whether packages description will render correctly on PyPI.
|
||||
echo 'Now checking PyPI description rendering...'
|
||||
if twine check dist/*.{tar.gz,whl}; then
|
||||
if twine check ./dist/*.{tar.gz,whl}; then
|
||||
echo -n 'Upload source and wheel distribution packages to PyPI ? [y/N] '
|
||||
read -r -n 1 -p '' && echo
|
||||
if [[ "$REPLY" =~ ^[yY]$ ]]; then
|
||||
echo 'Now signing & uploading source TAR and WHEEL to PyPI...'
|
||||
twine upload \
|
||||
--sign --identity "$GPG_IDENTITY" \
|
||||
dist/*.{tar.gz,whl}
|
||||
./dist/*.{tar.gz,whl}
|
||||
fi
|
||||
fi
|
||||
|
2
setup.py
2
setup.py
@ -23,6 +23,7 @@ setup(
|
||||
license='GPLv3',
|
||||
packages=find_packages(exclude=['archey.test']),
|
||||
test_suite='archey.test',
|
||||
python_requires='>=3.4',
|
||||
install_requires=[
|
||||
'distro',
|
||||
'netifaces'
|
||||
@ -60,6 +61,7 @@ Remain *maintained*, *community-driven* and *highly-compatible* with yesterday's
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Topic :: System'
|
||||
]
|
||||
)
|
||||
|
Reference in New Issue
Block a user