python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_vendor/
distro/
distro.py
       1  #!/usr/bin/env python
       2  # Copyright 2015,2016,2017 Nir Cohen
       3  #
       4  # Licensed under the Apache License, Version 2.0 (the "License");
       5  # you may not use this file except in compliance with the License.
       6  # You may obtain a copy of the License at
       7  #
       8  # http://www.apache.org/licenses/LICENSE-2.0
       9  #
      10  # Unless required by applicable law or agreed to in writing, software
      11  # distributed under the License is distributed on an "AS IS" BASIS,
      12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13  # See the License for the specific language governing permissions and
      14  # limitations under the License.
      15  
      16  """
      17  The ``distro`` package (``distro`` stands for Linux Distribution) provides
      18  information about the Linux distribution it runs on, such as a reliable
      19  machine-readable distro ID, or version information.
      20  
      21  It is the recommended replacement for Python's original
      22  :py:func:`platform.linux_distribution` function, but it provides much more
      23  functionality. An alternative implementation became necessary because Python
      24  3.5 deprecated this function, and Python 3.8 removed it altogether. Its
      25  predecessor function :py:func:`platform.dist` was already deprecated since
      26  Python 2.6 and removed in Python 3.8. Still, there are many cases in which
      27  access to OS distribution information is needed. See `Python issue 1322
      28  <https://bugs.python.org/issue1322>`_ for more information.
      29  """
      30  
      31  import argparse
      32  import json
      33  import logging
      34  import os
      35  import re
      36  import shlex
      37  import subprocess
      38  import sys
      39  import warnings
      40  from typing import (
      41      Any,
      42      Callable,
      43      Dict,
      44      Iterable,
      45      Optional,
      46      Sequence,
      47      TextIO,
      48      Tuple,
      49      Type,
      50  )
      51  
      52  try:
      53      from typing import TypedDict
      54  except ImportError:
      55      # Python 3.7
      56      TypedDict = dict
      57  
      58  __version__ = "1.8.0"
      59  
      60  
      61  class ESC[4;38;5;81mVersionDict(ESC[4;38;5;149mTypedDict):
      62      major: str
      63      minor: str
      64      build_number: str
      65  
      66  
      67  class ESC[4;38;5;81mInfoDict(ESC[4;38;5;149mTypedDict):
      68      id: str
      69      version: str
      70      version_parts: VersionDict
      71      like: str
      72      codename: str
      73  
      74  
      75  _UNIXCONFDIR = os.environ.get("UNIXCONFDIR", "/etc")
      76  _UNIXUSRLIBDIR = os.environ.get("UNIXUSRLIBDIR", "/usr/lib")
      77  _OS_RELEASE_BASENAME = "os-release"
      78  
      79  #: Translation table for normalizing the "ID" attribute defined in os-release
      80  #: files, for use by the :func:`distro.id` method.
      81  #:
      82  #: * Key: Value as defined in the os-release file, translated to lower case,
      83  #:   with blanks translated to underscores.
      84  #:
      85  #: * Value: Normalized value.
      86  NORMALIZED_OS_ID = {
      87      "ol": "oracle",  # Oracle Linux
      88      "opensuse-leap": "opensuse",  # Newer versions of OpenSuSE report as opensuse-leap
      89  }
      90  
      91  #: Translation table for normalizing the "Distributor ID" attribute returned by
      92  #: the lsb_release command, for use by the :func:`distro.id` method.
      93  #:
      94  #: * Key: Value as returned by the lsb_release command, translated to lower
      95  #:   case, with blanks translated to underscores.
      96  #:
      97  #: * Value: Normalized value.
      98  NORMALIZED_LSB_ID = {
      99      "enterpriseenterpriseas": "oracle",  # Oracle Enterprise Linux 4
     100      "enterpriseenterpriseserver": "oracle",  # Oracle Linux 5
     101      "redhatenterpriseworkstation": "rhel",  # RHEL 6, 7 Workstation
     102      "redhatenterpriseserver": "rhel",  # RHEL 6, 7 Server
     103      "redhatenterprisecomputenode": "rhel",  # RHEL 6 ComputeNode
     104  }
     105  
     106  #: Translation table for normalizing the distro ID derived from the file name
     107  #: of distro release files, for use by the :func:`distro.id` method.
     108  #:
     109  #: * Key: Value as derived from the file name of a distro release file,
     110  #:   translated to lower case, with blanks translated to underscores.
     111  #:
     112  #: * Value: Normalized value.
     113  NORMALIZED_DISTRO_ID = {
     114      "redhat": "rhel",  # RHEL 6.x, 7.x
     115  }
     116  
     117  # Pattern for content of distro release file (reversed)
     118  _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN = re.compile(
     119      r"(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)"
     120  )
     121  
     122  # Pattern for base file name of distro release file
     123  _DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$")
     124  
     125  # Base file names to be looked up for if _UNIXCONFDIR is not readable.
     126  _DISTRO_RELEASE_BASENAMES = [
     127      "SuSE-release",
     128      "arch-release",
     129      "base-release",
     130      "centos-release",
     131      "fedora-release",
     132      "gentoo-release",
     133      "mageia-release",
     134      "mandrake-release",
     135      "mandriva-release",
     136      "mandrivalinux-release",
     137      "manjaro-release",
     138      "oracle-release",
     139      "redhat-release",
     140      "rocky-release",
     141      "sl-release",
     142      "slackware-version",
     143  ]
     144  
     145  # Base file names to be ignored when searching for distro release file
     146  _DISTRO_RELEASE_IGNORE_BASENAMES = (
     147      "debian_version",
     148      "lsb-release",
     149      "oem-release",
     150      _OS_RELEASE_BASENAME,
     151      "system-release",
     152      "plesk-release",
     153      "iredmail-release",
     154  )
     155  
     156  
     157  def linux_distribution(full_distribution_name: bool = True) -> Tuple[str, str, str]:
     158      """
     159      .. deprecated:: 1.6.0
     160  
     161          :func:`distro.linux_distribution()` is deprecated. It should only be
     162          used as a compatibility shim with Python's
     163          :py:func:`platform.linux_distribution()`. Please use :func:`distro.id`,
     164          :func:`distro.version` and :func:`distro.name` instead.
     165  
     166      Return information about the current OS distribution as a tuple
     167      ``(id_name, version, codename)`` with items as follows:
     168  
     169      * ``id_name``:  If *full_distribution_name* is false, the result of
     170        :func:`distro.id`. Otherwise, the result of :func:`distro.name`.
     171  
     172      * ``version``:  The result of :func:`distro.version`.
     173  
     174      * ``codename``:  The extra item (usually in parentheses) after the
     175        os-release version number, or the result of :func:`distro.codename`.
     176  
     177      The interface of this function is compatible with the original
     178      :py:func:`platform.linux_distribution` function, supporting a subset of
     179      its parameters.
     180  
     181      The data it returns may not exactly be the same, because it uses more data
     182      sources than the original function, and that may lead to different data if
     183      the OS distribution is not consistent across multiple data sources it
     184      provides (there are indeed such distributions ...).
     185  
     186      Another reason for differences is the fact that the :func:`distro.id`
     187      method normalizes the distro ID string to a reliable machine-readable value
     188      for a number of popular OS distributions.
     189      """
     190      warnings.warn(
     191          "distro.linux_distribution() is deprecated. It should only be used as a "
     192          "compatibility shim with Python's platform.linux_distribution(). Please use "
     193          "distro.id(), distro.version() and distro.name() instead.",
     194          DeprecationWarning,
     195          stacklevel=2,
     196      )
     197      return _distro.linux_distribution(full_distribution_name)
     198  
     199  
     200  def id() -> str:
     201      """
     202      Return the distro ID of the current distribution, as a
     203      machine-readable string.
     204  
     205      For a number of OS distributions, the returned distro ID value is
     206      *reliable*, in the sense that it is documented and that it does not change
     207      across releases of the distribution.
     208  
     209      This package maintains the following reliable distro ID values:
     210  
     211      ==============  =========================================
     212      Distro ID       Distribution
     213      ==============  =========================================
     214      "ubuntu"        Ubuntu
     215      "debian"        Debian
     216      "rhel"          RedHat Enterprise Linux
     217      "centos"        CentOS
     218      "fedora"        Fedora
     219      "sles"          SUSE Linux Enterprise Server
     220      "opensuse"      openSUSE
     221      "amzn"          Amazon Linux
     222      "arch"          Arch Linux
     223      "buildroot"     Buildroot
     224      "cloudlinux"    CloudLinux OS
     225      "exherbo"       Exherbo Linux
     226      "gentoo"        GenToo Linux
     227      "ibm_powerkvm"  IBM PowerKVM
     228      "kvmibm"        KVM for IBM z Systems
     229      "linuxmint"     Linux Mint
     230      "mageia"        Mageia
     231      "mandriva"      Mandriva Linux
     232      "parallels"     Parallels
     233      "pidora"        Pidora
     234      "raspbian"      Raspbian
     235      "oracle"        Oracle Linux (and Oracle Enterprise Linux)
     236      "scientific"    Scientific Linux
     237      "slackware"     Slackware
     238      "xenserver"     XenServer
     239      "openbsd"       OpenBSD
     240      "netbsd"        NetBSD
     241      "freebsd"       FreeBSD
     242      "midnightbsd"   MidnightBSD
     243      "rocky"         Rocky Linux
     244      "aix"           AIX
     245      "guix"          Guix System
     246      ==============  =========================================
     247  
     248      If you have a need to get distros for reliable IDs added into this set,
     249      or if you find that the :func:`distro.id` function returns a different
     250      distro ID for one of the listed distros, please create an issue in the
     251      `distro issue tracker`_.
     252  
     253      **Lookup hierarchy and transformations:**
     254  
     255      First, the ID is obtained from the following sources, in the specified
     256      order. The first available and non-empty value is used:
     257  
     258      * the value of the "ID" attribute of the os-release file,
     259  
     260      * the value of the "Distributor ID" attribute returned by the lsb_release
     261        command,
     262  
     263      * the first part of the file name of the distro release file,
     264  
     265      The so determined ID value then passes the following transformations,
     266      before it is returned by this method:
     267  
     268      * it is translated to lower case,
     269  
     270      * blanks (which should not be there anyway) are translated to underscores,
     271  
     272      * a normalization of the ID is performed, based upon
     273        `normalization tables`_. The purpose of this normalization is to ensure
     274        that the ID is as reliable as possible, even across incompatible changes
     275        in the OS distributions. A common reason for an incompatible change is
     276        the addition of an os-release file, or the addition of the lsb_release
     277        command, with ID values that differ from what was previously determined
     278        from the distro release file name.
     279      """
     280      return _distro.id()
     281  
     282  
     283  def name(pretty: bool = False) -> str:
     284      """
     285      Return the name of the current OS distribution, as a human-readable
     286      string.
     287  
     288      If *pretty* is false, the name is returned without version or codename.
     289      (e.g. "CentOS Linux")
     290  
     291      If *pretty* is true, the version and codename are appended.
     292      (e.g. "CentOS Linux 7.1.1503 (Core)")
     293  
     294      **Lookup hierarchy:**
     295  
     296      The name is obtained from the following sources, in the specified order.
     297      The first available and non-empty value is used:
     298  
     299      * If *pretty* is false:
     300  
     301        - the value of the "NAME" attribute of the os-release file,
     302  
     303        - the value of the "Distributor ID" attribute returned by the lsb_release
     304          command,
     305  
     306        - the value of the "<name>" field of the distro release file.
     307  
     308      * If *pretty* is true:
     309  
     310        - the value of the "PRETTY_NAME" attribute of the os-release file,
     311  
     312        - the value of the "Description" attribute returned by the lsb_release
     313          command,
     314  
     315        - the value of the "<name>" field of the distro release file, appended
     316          with the value of the pretty version ("<version_id>" and "<codename>"
     317          fields) of the distro release file, if available.
     318      """
     319      return _distro.name(pretty)
     320  
     321  
     322  def version(pretty: bool = False, best: bool = False) -> str:
     323      """
     324      Return the version of the current OS distribution, as a human-readable
     325      string.
     326  
     327      If *pretty* is false, the version is returned without codename (e.g.
     328      "7.0").
     329  
     330      If *pretty* is true, the codename in parenthesis is appended, if the
     331      codename is non-empty (e.g. "7.0 (Maipo)").
     332  
     333      Some distributions provide version numbers with different precisions in
     334      the different sources of distribution information. Examining the different
     335      sources in a fixed priority order does not always yield the most precise
     336      version (e.g. for Debian 8.2, or CentOS 7.1).
     337  
     338      Some other distributions may not provide this kind of information. In these
     339      cases, an empty string would be returned. This behavior can be observed
     340      with rolling releases distributions (e.g. Arch Linux).
     341  
     342      The *best* parameter can be used to control the approach for the returned
     343      version:
     344  
     345      If *best* is false, the first non-empty version number in priority order of
     346      the examined sources is returned.
     347  
     348      If *best* is true, the most precise version number out of all examined
     349      sources is returned.
     350  
     351      **Lookup hierarchy:**
     352  
     353      In all cases, the version number is obtained from the following sources.
     354      If *best* is false, this order represents the priority order:
     355  
     356      * the value of the "VERSION_ID" attribute of the os-release file,
     357      * the value of the "Release" attribute returned by the lsb_release
     358        command,
     359      * the version number parsed from the "<version_id>" field of the first line
     360        of the distro release file,
     361      * the version number parsed from the "PRETTY_NAME" attribute of the
     362        os-release file, if it follows the format of the distro release files.
     363      * the version number parsed from the "Description" attribute returned by
     364        the lsb_release command, if it follows the format of the distro release
     365        files.
     366      """
     367      return _distro.version(pretty, best)
     368  
     369  
     370  def version_parts(best: bool = False) -> Tuple[str, str, str]:
     371      """
     372      Return the version of the current OS distribution as a tuple
     373      ``(major, minor, build_number)`` with items as follows:
     374  
     375      * ``major``:  The result of :func:`distro.major_version`.
     376  
     377      * ``minor``:  The result of :func:`distro.minor_version`.
     378  
     379      * ``build_number``:  The result of :func:`distro.build_number`.
     380  
     381      For a description of the *best* parameter, see the :func:`distro.version`
     382      method.
     383      """
     384      return _distro.version_parts(best)
     385  
     386  
     387  def major_version(best: bool = False) -> str:
     388      """
     389      Return the major version of the current OS distribution, as a string,
     390      if provided.
     391      Otherwise, the empty string is returned. The major version is the first
     392      part of the dot-separated version string.
     393  
     394      For a description of the *best* parameter, see the :func:`distro.version`
     395      method.
     396      """
     397      return _distro.major_version(best)
     398  
     399  
     400  def minor_version(best: bool = False) -> str:
     401      """
     402      Return the minor version of the current OS distribution, as a string,
     403      if provided.
     404      Otherwise, the empty string is returned. The minor version is the second
     405      part of the dot-separated version string.
     406  
     407      For a description of the *best* parameter, see the :func:`distro.version`
     408      method.
     409      """
     410      return _distro.minor_version(best)
     411  
     412  
     413  def build_number(best: bool = False) -> str:
     414      """
     415      Return the build number of the current OS distribution, as a string,
     416      if provided.
     417      Otherwise, the empty string is returned. The build number is the third part
     418      of the dot-separated version string.
     419  
     420      For a description of the *best* parameter, see the :func:`distro.version`
     421      method.
     422      """
     423      return _distro.build_number(best)
     424  
     425  
     426  def like() -> str:
     427      """
     428      Return a space-separated list of distro IDs of distributions that are
     429      closely related to the current OS distribution in regards to packaging
     430      and programming interfaces, for example distributions the current
     431      distribution is a derivative from.
     432  
     433      **Lookup hierarchy:**
     434  
     435      This information item is only provided by the os-release file.
     436      For details, see the description of the "ID_LIKE" attribute in the
     437      `os-release man page
     438      <http://www.freedesktop.org/software/systemd/man/os-release.html>`_.
     439      """
     440      return _distro.like()
     441  
     442  
     443  def codename() -> str:
     444      """
     445      Return the codename for the release of the current OS distribution,
     446      as a string.
     447  
     448      If the distribution does not have a codename, an empty string is returned.
     449  
     450      Note that the returned codename is not always really a codename. For
     451      example, openSUSE returns "x86_64". This function does not handle such
     452      cases in any special way and just returns the string it finds, if any.
     453  
     454      **Lookup hierarchy:**
     455  
     456      * the codename within the "VERSION" attribute of the os-release file, if
     457        provided,
     458  
     459      * the value of the "Codename" attribute returned by the lsb_release
     460        command,
     461  
     462      * the value of the "<codename>" field of the distro release file.
     463      """
     464      return _distro.codename()
     465  
     466  
     467  def info(pretty: bool = False, best: bool = False) -> InfoDict:
     468      """
     469      Return certain machine-readable information items about the current OS
     470      distribution in a dictionary, as shown in the following example:
     471  
     472      .. sourcecode:: python
     473  
     474          {
     475              'id': 'rhel',
     476              'version': '7.0',
     477              'version_parts': {
     478                  'major': '7',
     479                  'minor': '0',
     480                  'build_number': ''
     481              },
     482              'like': 'fedora',
     483              'codename': 'Maipo'
     484          }
     485  
     486      The dictionary structure and keys are always the same, regardless of which
     487      information items are available in the underlying data sources. The values
     488      for the various keys are as follows:
     489  
     490      * ``id``:  The result of :func:`distro.id`.
     491  
     492      * ``version``:  The result of :func:`distro.version`.
     493  
     494      * ``version_parts -> major``:  The result of :func:`distro.major_version`.
     495  
     496      * ``version_parts -> minor``:  The result of :func:`distro.minor_version`.
     497  
     498      * ``version_parts -> build_number``:  The result of
     499        :func:`distro.build_number`.
     500  
     501      * ``like``:  The result of :func:`distro.like`.
     502  
     503      * ``codename``:  The result of :func:`distro.codename`.
     504  
     505      For a description of the *pretty* and *best* parameters, see the
     506      :func:`distro.version` method.
     507      """
     508      return _distro.info(pretty, best)
     509  
     510  
     511  def os_release_info() -> Dict[str, str]:
     512      """
     513      Return a dictionary containing key-value pairs for the information items
     514      from the os-release file data source of the current OS distribution.
     515  
     516      See `os-release file`_ for details about these information items.
     517      """
     518      return _distro.os_release_info()
     519  
     520  
     521  def lsb_release_info() -> Dict[str, str]:
     522      """
     523      Return a dictionary containing key-value pairs for the information items
     524      from the lsb_release command data source of the current OS distribution.
     525  
     526      See `lsb_release command output`_ for details about these information
     527      items.
     528      """
     529      return _distro.lsb_release_info()
     530  
     531  
     532  def distro_release_info() -> Dict[str, str]:
     533      """
     534      Return a dictionary containing key-value pairs for the information items
     535      from the distro release file data source of the current OS distribution.
     536  
     537      See `distro release file`_ for details about these information items.
     538      """
     539      return _distro.distro_release_info()
     540  
     541  
     542  def uname_info() -> Dict[str, str]:
     543      """
     544      Return a dictionary containing key-value pairs for the information items
     545      from the distro release file data source of the current OS distribution.
     546      """
     547      return _distro.uname_info()
     548  
     549  
     550  def os_release_attr(attribute: str) -> str:
     551      """
     552      Return a single named information item from the os-release file data source
     553      of the current OS distribution.
     554  
     555      Parameters:
     556  
     557      * ``attribute`` (string): Key of the information item.
     558  
     559      Returns:
     560  
     561      * (string): Value of the information item, if the item exists.
     562        The empty string, if the item does not exist.
     563  
     564      See `os-release file`_ for details about these information items.
     565      """
     566      return _distro.os_release_attr(attribute)
     567  
     568  
     569  def lsb_release_attr(attribute: str) -> str:
     570      """
     571      Return a single named information item from the lsb_release command output
     572      data source of the current OS distribution.
     573  
     574      Parameters:
     575  
     576      * ``attribute`` (string): Key of the information item.
     577  
     578      Returns:
     579  
     580      * (string): Value of the information item, if the item exists.
     581        The empty string, if the item does not exist.
     582  
     583      See `lsb_release command output`_ for details about these information
     584      items.
     585      """
     586      return _distro.lsb_release_attr(attribute)
     587  
     588  
     589  def distro_release_attr(attribute: str) -> str:
     590      """
     591      Return a single named information item from the distro release file
     592      data source of the current OS distribution.
     593  
     594      Parameters:
     595  
     596      * ``attribute`` (string): Key of the information item.
     597  
     598      Returns:
     599  
     600      * (string): Value of the information item, if the item exists.
     601        The empty string, if the item does not exist.
     602  
     603      See `distro release file`_ for details about these information items.
     604      """
     605      return _distro.distro_release_attr(attribute)
     606  
     607  
     608  def uname_attr(attribute: str) -> str:
     609      """
     610      Return a single named information item from the distro release file
     611      data source of the current OS distribution.
     612  
     613      Parameters:
     614  
     615      * ``attribute`` (string): Key of the information item.
     616  
     617      Returns:
     618  
     619      * (string): Value of the information item, if the item exists.
     620                  The empty string, if the item does not exist.
     621      """
     622      return _distro.uname_attr(attribute)
     623  
     624  
     625  try:
     626      from functools import cached_property
     627  except ImportError:
     628      # Python < 3.8
     629      class ESC[4;38;5;81mcached_property:  # type: ignore
     630          """A version of @property which caches the value.  On access, it calls the
     631          underlying function and sets the value in `__dict__` so future accesses
     632          will not re-call the property.
     633          """
     634  
     635          def __init__(self, f: Callable[[Any], Any]) -> None:
     636              self._fname = f.__name__
     637              self._f = f
     638  
     639          def __get__(self, obj: Any, owner: Type[Any]) -> Any:
     640              assert obj is not None, f"call {self._fname} on an instance"
     641              ret = obj.__dict__[self._fname] = self._f(obj)
     642              return ret
     643  
     644  
     645  class ESC[4;38;5;81mLinuxDistribution:
     646      """
     647      Provides information about a OS distribution.
     648  
     649      This package creates a private module-global instance of this class with
     650      default initialization arguments, that is used by the
     651      `consolidated accessor functions`_ and `single source accessor functions`_.
     652      By using default initialization arguments, that module-global instance
     653      returns data about the current OS distribution (i.e. the distro this
     654      package runs on).
     655  
     656      Normally, it is not necessary to create additional instances of this class.
     657      However, in situations where control is needed over the exact data sources
     658      that are used, instances of this class can be created with a specific
     659      distro release file, or a specific os-release file, or without invoking the
     660      lsb_release command.
     661      """
     662  
     663      def __init__(
     664          self,
     665          include_lsb: Optional[bool] = None,
     666          os_release_file: str = "",
     667          distro_release_file: str = "",
     668          include_uname: Optional[bool] = None,
     669          root_dir: Optional[str] = None,
     670          include_oslevel: Optional[bool] = None,
     671      ) -> None:
     672          """
     673          The initialization method of this class gathers information from the
     674          available data sources, and stores that in private instance attributes.
     675          Subsequent access to the information items uses these private instance
     676          attributes, so that the data sources are read only once.
     677  
     678          Parameters:
     679  
     680          * ``include_lsb`` (bool): Controls whether the
     681            `lsb_release command output`_ is included as a data source.
     682  
     683            If the lsb_release command is not available in the program execution
     684            path, the data source for the lsb_release command will be empty.
     685  
     686          * ``os_release_file`` (string): The path name of the
     687            `os-release file`_ that is to be used as a data source.
     688  
     689            An empty string (the default) will cause the default path name to
     690            be used (see `os-release file`_ for details).
     691  
     692            If the specified or defaulted os-release file does not exist, the
     693            data source for the os-release file will be empty.
     694  
     695          * ``distro_release_file`` (string): The path name of the
     696            `distro release file`_ that is to be used as a data source.
     697  
     698            An empty string (the default) will cause a default search algorithm
     699            to be used (see `distro release file`_ for details).
     700  
     701            If the specified distro release file does not exist, or if no default
     702            distro release file can be found, the data source for the distro
     703            release file will be empty.
     704  
     705          * ``include_uname`` (bool): Controls whether uname command output is
     706            included as a data source. If the uname command is not available in
     707            the program execution path the data source for the uname command will
     708            be empty.
     709  
     710          * ``root_dir`` (string): The absolute path to the root directory to use
     711            to find distro-related information files. Note that ``include_*``
     712            parameters must not be enabled in combination with ``root_dir``.
     713  
     714          * ``include_oslevel`` (bool): Controls whether (AIX) oslevel command
     715            output is included as a data source. If the oslevel command is not
     716            available in the program execution path the data source will be
     717            empty.
     718  
     719          Public instance attributes:
     720  
     721          * ``os_release_file`` (string): The path name of the
     722            `os-release file`_ that is actually used as a data source. The
     723            empty string if no distro release file is used as a data source.
     724  
     725          * ``distro_release_file`` (string): The path name of the
     726            `distro release file`_ that is actually used as a data source. The
     727            empty string if no distro release file is used as a data source.
     728  
     729          * ``include_lsb`` (bool): The result of the ``include_lsb`` parameter.
     730            This controls whether the lsb information will be loaded.
     731  
     732          * ``include_uname`` (bool): The result of the ``include_uname``
     733            parameter. This controls whether the uname information will
     734            be loaded.
     735  
     736          * ``include_oslevel`` (bool): The result of the ``include_oslevel``
     737            parameter. This controls whether (AIX) oslevel information will be
     738            loaded.
     739  
     740          * ``root_dir`` (string): The result of the ``root_dir`` parameter.
     741            The absolute path to the root directory to use to find distro-related
     742            information files.
     743  
     744          Raises:
     745  
     746          * :py:exc:`ValueError`: Initialization parameters combination is not
     747             supported.
     748  
     749          * :py:exc:`OSError`: Some I/O issue with an os-release file or distro
     750            release file.
     751  
     752          * :py:exc:`UnicodeError`: A data source has unexpected characters or
     753            uses an unexpected encoding.
     754          """
     755          self.root_dir = root_dir
     756          self.etc_dir = os.path.join(root_dir, "etc") if root_dir else _UNIXCONFDIR
     757          self.usr_lib_dir = (
     758              os.path.join(root_dir, "usr/lib") if root_dir else _UNIXUSRLIBDIR
     759          )
     760  
     761          if os_release_file:
     762              self.os_release_file = os_release_file
     763          else:
     764              etc_dir_os_release_file = os.path.join(self.etc_dir, _OS_RELEASE_BASENAME)
     765              usr_lib_os_release_file = os.path.join(
     766                  self.usr_lib_dir, _OS_RELEASE_BASENAME
     767              )
     768  
     769              # NOTE: The idea is to respect order **and** have it set
     770              #       at all times for API backwards compatibility.
     771              if os.path.isfile(etc_dir_os_release_file) or not os.path.isfile(
     772                  usr_lib_os_release_file
     773              ):
     774                  self.os_release_file = etc_dir_os_release_file
     775              else:
     776                  self.os_release_file = usr_lib_os_release_file
     777  
     778          self.distro_release_file = distro_release_file or ""  # updated later
     779  
     780          is_root_dir_defined = root_dir is not None
     781          if is_root_dir_defined and (include_lsb or include_uname or include_oslevel):
     782              raise ValueError(
     783                  "Including subprocess data sources from specific root_dir is disallowed"
     784                  " to prevent false information"
     785              )
     786          self.include_lsb = (
     787              include_lsb if include_lsb is not None else not is_root_dir_defined
     788          )
     789          self.include_uname = (
     790              include_uname if include_uname is not None else not is_root_dir_defined
     791          )
     792          self.include_oslevel = (
     793              include_oslevel if include_oslevel is not None else not is_root_dir_defined
     794          )
     795  
     796      def __repr__(self) -> str:
     797          """Return repr of all info"""
     798          return (
     799              "LinuxDistribution("
     800              "os_release_file={self.os_release_file!r}, "
     801              "distro_release_file={self.distro_release_file!r}, "
     802              "include_lsb={self.include_lsb!r}, "
     803              "include_uname={self.include_uname!r}, "
     804              "include_oslevel={self.include_oslevel!r}, "
     805              "root_dir={self.root_dir!r}, "
     806              "_os_release_info={self._os_release_info!r}, "
     807              "_lsb_release_info={self._lsb_release_info!r}, "
     808              "_distro_release_info={self._distro_release_info!r}, "
     809              "_uname_info={self._uname_info!r}, "
     810              "_oslevel_info={self._oslevel_info!r})".format(self=self)
     811          )
     812  
     813      def linux_distribution(
     814          self, full_distribution_name: bool = True
     815      ) -> Tuple[str, str, str]:
     816          """
     817          Return information about the OS distribution that is compatible
     818          with Python's :func:`platform.linux_distribution`, supporting a subset
     819          of its parameters.
     820  
     821          For details, see :func:`distro.linux_distribution`.
     822          """
     823          return (
     824              self.name() if full_distribution_name else self.id(),
     825              self.version(),
     826              self._os_release_info.get("release_codename") or self.codename(),
     827          )
     828  
     829      def id(self) -> str:
     830          """Return the distro ID of the OS distribution, as a string.
     831  
     832          For details, see :func:`distro.id`.
     833          """
     834  
     835          def normalize(distro_id: str, table: Dict[str, str]) -> str:
     836              distro_id = distro_id.lower().replace(" ", "_")
     837              return table.get(distro_id, distro_id)
     838  
     839          distro_id = self.os_release_attr("id")
     840          if distro_id:
     841              return normalize(distro_id, NORMALIZED_OS_ID)
     842  
     843          distro_id = self.lsb_release_attr("distributor_id")
     844          if distro_id:
     845              return normalize(distro_id, NORMALIZED_LSB_ID)
     846  
     847          distro_id = self.distro_release_attr("id")
     848          if distro_id:
     849              return normalize(distro_id, NORMALIZED_DISTRO_ID)
     850  
     851          distro_id = self.uname_attr("id")
     852          if distro_id:
     853              return normalize(distro_id, NORMALIZED_DISTRO_ID)
     854  
     855          return ""
     856  
     857      def name(self, pretty: bool = False) -> str:
     858          """
     859          Return the name of the OS distribution, as a string.
     860  
     861          For details, see :func:`distro.name`.
     862          """
     863          name = (
     864              self.os_release_attr("name")
     865              or self.lsb_release_attr("distributor_id")
     866              or self.distro_release_attr("name")
     867              or self.uname_attr("name")
     868          )
     869          if pretty:
     870              name = self.os_release_attr("pretty_name") or self.lsb_release_attr(
     871                  "description"
     872              )
     873              if not name:
     874                  name = self.distro_release_attr("name") or self.uname_attr("name")
     875                  version = self.version(pretty=True)
     876                  if version:
     877                      name = f"{name} {version}"
     878          return name or ""
     879  
     880      def version(self, pretty: bool = False, best: bool = False) -> str:
     881          """
     882          Return the version of the OS distribution, as a string.
     883  
     884          For details, see :func:`distro.version`.
     885          """
     886          versions = [
     887              self.os_release_attr("version_id"),
     888              self.lsb_release_attr("release"),
     889              self.distro_release_attr("version_id"),
     890              self._parse_distro_release_content(self.os_release_attr("pretty_name")).get(
     891                  "version_id", ""
     892              ),
     893              self._parse_distro_release_content(
     894                  self.lsb_release_attr("description")
     895              ).get("version_id", ""),
     896              self.uname_attr("release"),
     897          ]
     898          if self.uname_attr("id").startswith("aix"):
     899              # On AIX platforms, prefer oslevel command output.
     900              versions.insert(0, self.oslevel_info())
     901          elif self.id() == "debian" or "debian" in self.like().split():
     902              # On Debian-like, add debian_version file content to candidates list.
     903              versions.append(self._debian_version)
     904          version = ""
     905          if best:
     906              # This algorithm uses the last version in priority order that has
     907              # the best precision. If the versions are not in conflict, that
     908              # does not matter; otherwise, using the last one instead of the
     909              # first one might be considered a surprise.
     910              for v in versions:
     911                  if v.count(".") > version.count(".") or version == "":
     912                      version = v
     913          else:
     914              for v in versions:
     915                  if v != "":
     916                      version = v
     917                      break
     918          if pretty and version and self.codename():
     919              version = f"{version} ({self.codename()})"
     920          return version
     921  
     922      def version_parts(self, best: bool = False) -> Tuple[str, str, str]:
     923          """
     924          Return the version of the OS distribution, as a tuple of version
     925          numbers.
     926  
     927          For details, see :func:`distro.version_parts`.
     928          """
     929          version_str = self.version(best=best)
     930          if version_str:
     931              version_regex = re.compile(r"(\d+)\.?(\d+)?\.?(\d+)?")
     932              matches = version_regex.match(version_str)
     933              if matches:
     934                  major, minor, build_number = matches.groups()
     935                  return major, minor or "", build_number or ""
     936          return "", "", ""
     937  
     938      def major_version(self, best: bool = False) -> str:
     939          """
     940          Return the major version number of the current distribution.
     941  
     942          For details, see :func:`distro.major_version`.
     943          """
     944          return self.version_parts(best)[0]
     945  
     946      def minor_version(self, best: bool = False) -> str:
     947          """
     948          Return the minor version number of the current distribution.
     949  
     950          For details, see :func:`distro.minor_version`.
     951          """
     952          return self.version_parts(best)[1]
     953  
     954      def build_number(self, best: bool = False) -> str:
     955          """
     956          Return the build number of the current distribution.
     957  
     958          For details, see :func:`distro.build_number`.
     959          """
     960          return self.version_parts(best)[2]
     961  
     962      def like(self) -> str:
     963          """
     964          Return the IDs of distributions that are like the OS distribution.
     965  
     966          For details, see :func:`distro.like`.
     967          """
     968          return self.os_release_attr("id_like") or ""
     969  
     970      def codename(self) -> str:
     971          """
     972          Return the codename of the OS distribution.
     973  
     974          For details, see :func:`distro.codename`.
     975          """
     976          try:
     977              # Handle os_release specially since distros might purposefully set
     978              # this to empty string to have no codename
     979              return self._os_release_info["codename"]
     980          except KeyError:
     981              return (
     982                  self.lsb_release_attr("codename")
     983                  or self.distro_release_attr("codename")
     984                  or ""
     985              )
     986  
     987      def info(self, pretty: bool = False, best: bool = False) -> InfoDict:
     988          """
     989          Return certain machine-readable information about the OS
     990          distribution.
     991  
     992          For details, see :func:`distro.info`.
     993          """
     994          return dict(
     995              id=self.id(),
     996              version=self.version(pretty, best),
     997              version_parts=dict(
     998                  major=self.major_version(best),
     999                  minor=self.minor_version(best),
    1000                  build_number=self.build_number(best),
    1001              ),
    1002              like=self.like(),
    1003              codename=self.codename(),
    1004          )
    1005  
    1006      def os_release_info(self) -> Dict[str, str]:
    1007          """
    1008          Return a dictionary containing key-value pairs for the information
    1009          items from the os-release file data source of the OS distribution.
    1010  
    1011          For details, see :func:`distro.os_release_info`.
    1012          """
    1013          return self._os_release_info
    1014  
    1015      def lsb_release_info(self) -> Dict[str, str]:
    1016          """
    1017          Return a dictionary containing key-value pairs for the information
    1018          items from the lsb_release command data source of the OS
    1019          distribution.
    1020  
    1021          For details, see :func:`distro.lsb_release_info`.
    1022          """
    1023          return self._lsb_release_info
    1024  
    1025      def distro_release_info(self) -> Dict[str, str]:
    1026          """
    1027          Return a dictionary containing key-value pairs for the information
    1028          items from the distro release file data source of the OS
    1029          distribution.
    1030  
    1031          For details, see :func:`distro.distro_release_info`.
    1032          """
    1033          return self._distro_release_info
    1034  
    1035      def uname_info(self) -> Dict[str, str]:
    1036          """
    1037          Return a dictionary containing key-value pairs for the information
    1038          items from the uname command data source of the OS distribution.
    1039  
    1040          For details, see :func:`distro.uname_info`.
    1041          """
    1042          return self._uname_info
    1043  
    1044      def oslevel_info(self) -> str:
    1045          """
    1046          Return AIX' oslevel command output.
    1047          """
    1048          return self._oslevel_info
    1049  
    1050      def os_release_attr(self, attribute: str) -> str:
    1051          """
    1052          Return a single named information item from the os-release file data
    1053          source of the OS distribution.
    1054  
    1055          For details, see :func:`distro.os_release_attr`.
    1056          """
    1057          return self._os_release_info.get(attribute, "")
    1058  
    1059      def lsb_release_attr(self, attribute: str) -> str:
    1060          """
    1061          Return a single named information item from the lsb_release command
    1062          output data source of the OS distribution.
    1063  
    1064          For details, see :func:`distro.lsb_release_attr`.
    1065          """
    1066          return self._lsb_release_info.get(attribute, "")
    1067  
    1068      def distro_release_attr(self, attribute: str) -> str:
    1069          """
    1070          Return a single named information item from the distro release file
    1071          data source of the OS distribution.
    1072  
    1073          For details, see :func:`distro.distro_release_attr`.
    1074          """
    1075          return self._distro_release_info.get(attribute, "")
    1076  
    1077      def uname_attr(self, attribute: str) -> str:
    1078          """
    1079          Return a single named information item from the uname command
    1080          output data source of the OS distribution.
    1081  
    1082          For details, see :func:`distro.uname_attr`.
    1083          """
    1084          return self._uname_info.get(attribute, "")
    1085  
    1086      @cached_property
    1087      def _os_release_info(self) -> Dict[str, str]:
    1088          """
    1089          Get the information items from the specified os-release file.
    1090  
    1091          Returns:
    1092              A dictionary containing all information items.
    1093          """
    1094          if os.path.isfile(self.os_release_file):
    1095              with open(self.os_release_file, encoding="utf-8") as release_file:
    1096                  return self._parse_os_release_content(release_file)
    1097          return {}
    1098  
    1099      @staticmethod
    1100      def _parse_os_release_content(lines: TextIO) -> Dict[str, str]:
    1101          """
    1102          Parse the lines of an os-release file.
    1103  
    1104          Parameters:
    1105  
    1106          * lines: Iterable through the lines in the os-release file.
    1107                   Each line must be a unicode string or a UTF-8 encoded byte
    1108                   string.
    1109  
    1110          Returns:
    1111              A dictionary containing all information items.
    1112          """
    1113          props = {}
    1114          lexer = shlex.shlex(lines, posix=True)
    1115          lexer.whitespace_split = True
    1116  
    1117          tokens = list(lexer)
    1118          for token in tokens:
    1119              # At this point, all shell-like parsing has been done (i.e.
    1120              # comments processed, quotes and backslash escape sequences
    1121              # processed, multi-line values assembled, trailing newlines
    1122              # stripped, etc.), so the tokens are now either:
    1123              # * variable assignments: var=value
    1124              # * commands or their arguments (not allowed in os-release)
    1125              # Ignore any tokens that are not variable assignments
    1126              if "=" in token:
    1127                  k, v = token.split("=", 1)
    1128                  props[k.lower()] = v
    1129  
    1130          if "version" in props:
    1131              # extract release codename (if any) from version attribute
    1132              match = re.search(r"\((\D+)\)|,\s*(\D+)", props["version"])
    1133              if match:
    1134                  release_codename = match.group(1) or match.group(2)
    1135                  props["codename"] = props["release_codename"] = release_codename
    1136  
    1137          if "version_codename" in props:
    1138              # os-release added a version_codename field.  Use that in
    1139              # preference to anything else Note that some distros purposefully
    1140              # do not have code names.  They should be setting
    1141              # version_codename=""
    1142              props["codename"] = props["version_codename"]
    1143          elif "ubuntu_codename" in props:
    1144              # Same as above but a non-standard field name used on older Ubuntus
    1145              props["codename"] = props["ubuntu_codename"]
    1146  
    1147          return props
    1148  
    1149      @cached_property
    1150      def _lsb_release_info(self) -> Dict[str, str]:
    1151          """
    1152          Get the information items from the lsb_release command output.
    1153  
    1154          Returns:
    1155              A dictionary containing all information items.
    1156          """
    1157          if not self.include_lsb:
    1158              return {}
    1159          try:
    1160              cmd = ("lsb_release", "-a")
    1161              stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
    1162          # Command not found or lsb_release returned error
    1163          except (OSError, subprocess.CalledProcessError):
    1164              return {}
    1165          content = self._to_str(stdout).splitlines()
    1166          return self._parse_lsb_release_content(content)
    1167  
    1168      @staticmethod
    1169      def _parse_lsb_release_content(lines: Iterable[str]) -> Dict[str, str]:
    1170          """
    1171          Parse the output of the lsb_release command.
    1172  
    1173          Parameters:
    1174  
    1175          * lines: Iterable through the lines of the lsb_release output.
    1176                   Each line must be a unicode string or a UTF-8 encoded byte
    1177                   string.
    1178  
    1179          Returns:
    1180              A dictionary containing all information items.
    1181          """
    1182          props = {}
    1183          for line in lines:
    1184              kv = line.strip("\n").split(":", 1)
    1185              if len(kv) != 2:
    1186                  # Ignore lines without colon.
    1187                  continue
    1188              k, v = kv
    1189              props.update({k.replace(" ", "_").lower(): v.strip()})
    1190          return props
    1191  
    1192      @cached_property
    1193      def _uname_info(self) -> Dict[str, str]:
    1194          if not self.include_uname:
    1195              return {}
    1196          try:
    1197              cmd = ("uname", "-rs")
    1198              stdout = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
    1199          except OSError:
    1200              return {}
    1201          content = self._to_str(stdout).splitlines()
    1202          return self._parse_uname_content(content)
    1203  
    1204      @cached_property
    1205      def _oslevel_info(self) -> str:
    1206          if not self.include_oslevel:
    1207              return ""
    1208          try:
    1209              stdout = subprocess.check_output("oslevel", stderr=subprocess.DEVNULL)
    1210          except (OSError, subprocess.CalledProcessError):
    1211              return ""
    1212          return self._to_str(stdout).strip()
    1213  
    1214      @cached_property
    1215      def _debian_version(self) -> str:
    1216          try:
    1217              with open(
    1218                  os.path.join(self.etc_dir, "debian_version"), encoding="ascii"
    1219              ) as fp:
    1220                  return fp.readline().rstrip()
    1221          except FileNotFoundError:
    1222              return ""
    1223  
    1224      @staticmethod
    1225      def _parse_uname_content(lines: Sequence[str]) -> Dict[str, str]:
    1226          if not lines:
    1227              return {}
    1228          props = {}
    1229          match = re.search(r"^([^\s]+)\s+([\d\.]+)", lines[0].strip())
    1230          if match:
    1231              name, version = match.groups()
    1232  
    1233              # This is to prevent the Linux kernel version from
    1234              # appearing as the 'best' version on otherwise
    1235              # identifiable distributions.
    1236              if name == "Linux":
    1237                  return {}
    1238              props["id"] = name.lower()
    1239              props["name"] = name
    1240              props["release"] = version
    1241          return props
    1242  
    1243      @staticmethod
    1244      def _to_str(bytestring: bytes) -> str:
    1245          encoding = sys.getfilesystemencoding()
    1246          return bytestring.decode(encoding)
    1247  
    1248      @cached_property
    1249      def _distro_release_info(self) -> Dict[str, str]:
    1250          """
    1251          Get the information items from the specified distro release file.
    1252  
    1253          Returns:
    1254              A dictionary containing all information items.
    1255          """
    1256          if self.distro_release_file:
    1257              # If it was specified, we use it and parse what we can, even if
    1258              # its file name or content does not match the expected pattern.
    1259              distro_info = self._parse_distro_release_file(self.distro_release_file)
    1260              basename = os.path.basename(self.distro_release_file)
    1261              # The file name pattern for user-specified distro release files
    1262              # is somewhat more tolerant (compared to when searching for the
    1263              # file), because we want to use what was specified as best as
    1264              # possible.
    1265              match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename)
    1266          else:
    1267              try:
    1268                  basenames = [
    1269                      basename
    1270                      for basename in os.listdir(self.etc_dir)
    1271                      if basename not in _DISTRO_RELEASE_IGNORE_BASENAMES
    1272                      and os.path.isfile(os.path.join(self.etc_dir, basename))
    1273                  ]
    1274                  # We sort for repeatability in cases where there are multiple
    1275                  # distro specific files; e.g. CentOS, Oracle, Enterprise all
    1276                  # containing `redhat-release` on top of their own.
    1277                  basenames.sort()
    1278              except OSError:
    1279                  # This may occur when /etc is not readable but we can't be
    1280                  # sure about the *-release files. Check common entries of
    1281                  # /etc for information. If they turn out to not be there the
    1282                  # error is handled in `_parse_distro_release_file()`.
    1283                  basenames = _DISTRO_RELEASE_BASENAMES
    1284              for basename in basenames:
    1285                  match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename)
    1286                  if match is None:
    1287                      continue
    1288                  filepath = os.path.join(self.etc_dir, basename)
    1289                  distro_info = self._parse_distro_release_file(filepath)
    1290                  # The name is always present if the pattern matches.
    1291                  if "name" not in distro_info:
    1292                      continue
    1293                  self.distro_release_file = filepath
    1294                  break
    1295              else:  # the loop didn't "break": no candidate.
    1296                  return {}
    1297  
    1298          if match is not None:
    1299              distro_info["id"] = match.group(1)
    1300  
    1301          # CloudLinux < 7: manually enrich info with proper id.
    1302          if "cloudlinux" in distro_info.get("name", "").lower():
    1303              distro_info["id"] = "cloudlinux"
    1304  
    1305          return distro_info
    1306  
    1307      def _parse_distro_release_file(self, filepath: str) -> Dict[str, str]:
    1308          """
    1309          Parse a distro release file.
    1310  
    1311          Parameters:
    1312  
    1313          * filepath: Path name of the distro release file.
    1314  
    1315          Returns:
    1316              A dictionary containing all information items.
    1317          """
    1318          try:
    1319              with open(filepath, encoding="utf-8") as fp:
    1320                  # Only parse the first line. For instance, on SLES there
    1321                  # are multiple lines. We don't want them...
    1322                  return self._parse_distro_release_content(fp.readline())
    1323          except OSError:
    1324              # Ignore not being able to read a specific, seemingly version
    1325              # related file.
    1326              # See https://github.com/python-distro/distro/issues/162
    1327              return {}
    1328  
    1329      @staticmethod
    1330      def _parse_distro_release_content(line: str) -> Dict[str, str]:
    1331          """
    1332          Parse a line from a distro release file.
    1333  
    1334          Parameters:
    1335          * line: Line from the distro release file. Must be a unicode string
    1336                  or a UTF-8 encoded byte string.
    1337  
    1338          Returns:
    1339              A dictionary containing all information items.
    1340          """
    1341          matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match(line.strip()[::-1])
    1342          distro_info = {}
    1343          if matches:
    1344              # regexp ensures non-None
    1345              distro_info["name"] = matches.group(3)[::-1]
    1346              if matches.group(2):
    1347                  distro_info["version_id"] = matches.group(2)[::-1]
    1348              if matches.group(1):
    1349                  distro_info["codename"] = matches.group(1)[::-1]
    1350          elif line:
    1351              distro_info["name"] = line.strip()
    1352          return distro_info
    1353  
    1354  
    1355  _distro = LinuxDistribution()
    1356  
    1357  
    1358  def main() -> None:
    1359      logger = logging.getLogger(__name__)
    1360      logger.setLevel(logging.DEBUG)
    1361      logger.addHandler(logging.StreamHandler(sys.stdout))
    1362  
    1363      parser = argparse.ArgumentParser(description="OS distro info tool")
    1364      parser.add_argument(
    1365          "--json", "-j", help="Output in machine readable format", action="store_true"
    1366      )
    1367  
    1368      parser.add_argument(
    1369          "--root-dir",
    1370          "-r",
    1371          type=str,
    1372          dest="root_dir",
    1373          help="Path to the root filesystem directory (defaults to /)",
    1374      )
    1375  
    1376      args = parser.parse_args()
    1377  
    1378      if args.root_dir:
    1379          dist = LinuxDistribution(
    1380              include_lsb=False,
    1381              include_uname=False,
    1382              include_oslevel=False,
    1383              root_dir=args.root_dir,
    1384          )
    1385      else:
    1386          dist = _distro
    1387  
    1388      if args.json:
    1389          logger.info(json.dumps(dist.info(), indent=4, sort_keys=True))
    1390      else:
    1391          logger.info("Name: %s", dist.name(pretty=True))
    1392          distribution_version = dist.version(pretty=True)
    1393          logger.info("Version: %s", distribution_version)
    1394          distribution_codename = dist.codename()
    1395          logger.info("Codename: %s", distribution_codename)
    1396  
    1397  
    1398  if __name__ == "__main__":
    1399      main()