Skip to content

Info for contributors

Samuel FORESTIER edited this page Aug 25, 2024 · 16 revisions

Project structure

Code structure

Currently, Archey is written as a root Python package archey with modules for our core functionality:

  • api.py provides the JSON API, which may be extended in the future to support other notations.
  • colors.py provides functionality dealing with ANSI/ECMA escape codes for coloured text.
  • configuration.py provides a singleton Configuration class, which is hopefully self-explanatory, as long as default required options.
  • distributions.py contains the supported distributions and methods for detecting the system one.
  • entry.py is a base-class definition which is used by all of Archey's entries; more details below.
  • environment.py contains a singleton class which is our interface to deal with "global" environment variables altering Archey's behavior.
  • output.py has the Output class, which deals with all of Archey's output tasks.
  • processes.py contains a singleton class which gathers a list of all running processes on the system.
  • screenshot.py contains a standalone function which is responsible to find a way to take a screenshot ("best effort").
  • singleton.py contains the singleton meta-class used by Processes, Configuration, Environment & Utility.
  • utility.py contains a singleton class defining some internal utility functions used by Archey.

There are also sub-packages:

  • archey/entries -- Each module in this package corresponds to one Archey entry.
  • archey/logos -- Each module in this package contains a distribution's logo.

Directory structure

A brief overview of Archey's directory structure is:

.
├── .github/         // Project meta files related to GitHub repository
├── archey/          // Python module root directory
│   ├── entries/     // Code for each entry
│   ├── logos/       // ASCII art logos and colors, as Python modules
│   ├── test/        // Unit tests
│   ├── py.typed     // PEP-561 compliance file
│   └── *.py         // Entry points and main Python modules
├── dist/            // Packages and binaries generation target
├── packaging/       // Build scripts for packaging
├── pyproject.toml   // Project tools configuration file (PEP-518)
├── CHANGELOG.md     // Changelog info
├── COPYRIGHT.md     // Copyright info
├── LICENSE          // Project licensing
├── README.md        // Project description and documentation
├── archey.1         // *NIX manual page
├── config.json      // Default configuration file
└── setup.py         // Distutils packaging file

Writing entries or modules

Some useful things to know while writing code for Archey...

Configuration

If you're writing an entry, inheriting from the archey.entry.Entry class will provide you with configuration in the instance attribute options.

from archey.entry import Entry

class MyEntry(Entry):
    # Use this internal attribute if your entry should have a "pretty name" different than the class'.
    _PRETTY_NAME = 'My Entry'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # `self.options` contains specific configuration related to your entry.
        an_option_value = self.options.get('key')

        # For i18n, `default_strings` will also be propagated as a protected attribute.
        not_detected_str = self._default_strings.get('not_detected')

        # Starting with Archey >= 4.13.0, you may use a dedicated logger.
        self._logger.warning('This is a warning message.')

Otherwise, when writing/fixing an internal module of Archey, to get the (global) configuration set by the user, import the configuration module and simply instantiate the Configuration class:

from archey.configuration import Configuration

config = Configuration()
some_config_item = config.get('key')

This works at all times in all modules, since Configuration is a singleton which is populated when Archey launches.

Configuration is also an iterable, so you can get the whole thing as a dict if you really want:

from archey.configuration import Configuration

config_dict = dict(Configuration())

Basic structure of an Entry

The Entry base-class (seen in archey/entry.py) paints the basic picture of what an entry is. The simplest possible entry you could write is the following:

from archey.entry import Entry

class MyEntry(Entry):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # `name` defaults to entry's internal name.
        self.name = 'MyEntry'
        self.value = None

In an entry, the goal is to put a Python object into the value attribute which best represents the data you have. For example, the Hostname entry's value is simply a string containing your system host-name in it. A slightly more complex example is Distro, whose value is a dictionary with the keys name and arch, whose values are strings corresponding to the name and architecture of the detected distribution respectively.

When Archey is run, all entries are instantiated and either:

  • If running in normal mode, Output calls MyEntry.output to add the entry to the output.
  • If running in API mode (i.e. currently JSON output mode using -j), output is never called and the value attribute is serialized to JSON using Python's default serializer.

The default Entry base-class output method simply calls str() on value to get a string representation -- this method calls the __str__ method, falling back to repr(). If you want, for example, to output a dictionary with custom formatting, you have to override output. You should be able to do this easily by expanding on the basic method in the Entry base class.

Keep in mind:

  • Don't put important code in the output method that is required for a sane value attribute.
  • Don't put a custom class in value, otherwise we can't serialize it for JSON output!

Test guidelines

Archey uses the unittest Python module. All of our tests can be found in archey/test. Each file is named in the format test_archey_{module name}. All entry tests belong in the entries sub-folder.

All modules

Custom assertions

There is currently one helper class for all other modules; this is the CustomAssertions class (it can also be used in entry tests if desired). It is used by inheriting it in the testing class, e.g.:

import unittest

from archey.test import CustomAssertions

class TestMyModule(unittest.TestCase, CustomAssertions):
    # Custom assertions are now available in all tests in this class.
    # Call them the same way as `unittest`'s assertions.
    # e.g:
    def test_some_method(self):
        self.assertListEmpty(MyModule.empty_list_attribute)

The list of custom assertions is:

  • assertListEmpty(obj) - asserts that the supplied object:
    • is a list - isinstance(obj, list) == True
    • contains no items - (not obj) == True

Entries

There are some supplied helper methods in archey/test/entries/__init__.py to aid writing clean and stable entry tests. To use them, simply import them, e.g.:

from archey.test.entries import HelperMethods

entry_mock

Use to get a mock "instance" of an entry, e.g.:

CPU_mock_instance = HelperMethods.entry_mock(CPU)

This method is intended to aid in writing tests for instance methods of an entry. It creates a MagicMock which wraps all attributes of the entry, and acts like a "default" entry which inherits from archey.entry.Entry. That is to say, it always additionally has:

  • .name attribute equal to the class-name of the entry supplied.
  • .value attribute as None.
  • .options attribute - see below.
  • ._default_strings protected attribute, containing default i18n strings.
  • ._logger protected attribute, a dedicated logging.Logger object (Archey >= 4.13.0)

... and if I'm not a developer ?

Sure, you might :