1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2025-06-19 00:17:51 +02:00

Merge pull request #542 from kellyjonbrazil/dev

Dev v1.25.1
This commit is contained in:
Kelly Brazil
2024-02-13 00:35:00 +00:00
committed by GitHub
36 changed files with 1271 additions and 565 deletions

View File

@ -1,5 +1,14 @@
jc changelog jc changelog
20240212 v1.25.1
- Fix for crash when optional libraries are not installed (e.g. xmltodict)
- Fix for `ini` parser crashing with some keys with no values
- Fix `xrandr` parser to extract more EDID data
- Enhance `uptime` parser to support output with no user information
- Enhance `--quiet` CLI option to cover more warning messages
- Add tests for missing optional libraries
- Documentation updates
20240204 v1.25.0 20240204 v1.25.0
- Add `--slurp` functionality to wrap output from multiple lines into a single array. - Add `--slurp` functionality to wrap output from multiple lines into a single array.
Note, this only works with single-line input parsers. (e.g. `date`, `ip-address`, `url`, etc.) Note, this only works with single-line input parsers. (e.g. `date`, `ip-address`, `url`, etc.)

View File

@ -98,4 +98,4 @@ Compatibility: linux, darwin, cygwin, win32, aix, freebsd
Source: [`jc/parsers/ini.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/ini.py) Source: [`jc/parsers/ini.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/ini.py)
Version 2.1 by Kelly Brazil (kellyjonbrazil@gmail.com) Version 2.2 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@ -76,4 +76,4 @@ Compatibility: linux, darwin, cygwin, win32, aix, freebsd
Source: [`jc/parsers/plist.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/plist.py) Source: [`jc/parsers/plist.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/plist.py)
Version 1.1 by Kelly Brazil (kellyjonbrazil@gmail.com) Version 1.2 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@ -92,4 +92,4 @@ Source: [`jc/parsers/uptime.py`](https://github.com/kellyjonbrazil/jc/blob/maste
This parser can be used with the `--slurp` command-line option. This parser can be used with the `--slurp` command-line option.
Version 1.8 by Kelly Brazil (kellyjonbrazil@gmail.com) Version 1.9 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@ -100,4 +100,4 @@ Compatibility: linux, darwin, cygwin, win32, aix, freebsd
Source: [`jc/parsers/xml.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/xml.py) Source: [`jc/parsers/xml.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/xml.py)
Version 1.9 by Kelly Brazil (kellyjonbrazil@gmail.com) Version 1.10 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@ -33,7 +33,7 @@ Schema:
"maximum_height": integer, "maximum_height": integer,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": integer, "resolution_width": integer,
"resolution_height": integer, "resolution_height": integer,
@ -82,7 +82,7 @@ Examples:
"maximum_height": 32767, "maximum_height": 32767,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": 1920, "resolution_width": 1920,
"resolution_height": 1080, "resolution_height": 1080,
@ -143,7 +143,7 @@ Examples:
"maximum_height": 32767, "maximum_height": 32767,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": 1920, "resolution_width": 1920,
"resolution_height": 1080, "resolution_height": 1080,
@ -199,7 +199,7 @@ Examples:
### parse ### parse
```python ```python
def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict def parse(data: str, raw: bool = False, quiet: bool = False) -> Response
``` ```
Main text parsing function Main text parsing function
@ -219,4 +219,4 @@ Compatibility: linux, darwin, cygwin, aix, freebsd
Source: [`jc/parsers/xrandr.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/xrandr.py) Source: [`jc/parsers/xrandr.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/xrandr.py)
Version 1.4 by Kevin Lyter (code (at) lyterk.com) Version 2.0 by Kevin Lyter (code (at) lyterk.com)

View File

@ -408,7 +408,7 @@ class JcCli():
ensure_ascii=self.ascii_only ensure_ascii=self.ascii_only
) )
if not self.mono: if not self.mono and PYGMENTS_INSTALLED:
class JcStyle(Style): class JcStyle(Style):
styles: CustomColorType = self.custom_colors styles: CustomColorType = self.custom_colors
@ -540,13 +540,12 @@ class JcCli():
if self.magic_run_command_str.startswith('/proc'): if self.magic_run_command_str.startswith('/proc'):
try: try:
self.magic_found_parser = 'proc' self.magic_found_parser = 'proc'
filelist = shlex.split(self.magic_run_command_str)
# multiple proc files detected # multiple proc files detected
if ' ' in self.magic_run_command_str: if len(filelist) > 1:
self.slurp = True self.slurp = True
multi_out: List[str] = [] multi_out: List[str] = []
filelist = self.magic_run_command_str.split()
filelist = [x.strip() for x in filelist]
self.inputlist = filelist self.inputlist = filelist
for file in self.inputlist: for file in self.inputlist:
@ -557,7 +556,7 @@ class JcCli():
# single proc file # single proc file
else: else:
file = self.magic_run_command_str file = filelist[0]
# self.magic_stdout = self.open_text_file('/Users/kelly/temp' + file) # self.magic_stdout = self.open_text_file('/Users/kelly/temp' + file)
self.magic_stdout = self.open_text_file(file) self.magic_stdout = self.open_text_file(file)
@ -861,6 +860,9 @@ class JcCli():
self.set_mono() self.set_mono()
self.set_custom_colors() self.set_custom_colors()
if self.quiet:
utils.CLI_QUIET = True
if self.verbose_debug: if self.verbose_debug:
tracebackplus.enable(context=11) # type: ignore tracebackplus.enable(context=11) # type: ignore

View File

@ -101,7 +101,7 @@ Examples:
$ jc --pretty /proc/meminfo $ jc --pretty /proc/meminfo
Line Slicing: Line Slicing:
$ $ cat output.txt | jc 4:15 --parser # Parse from line 4 to 14 $ cat output.txt | jc 4:15 --parser # Parse from line 4 to 14
with parser (zero-based) with parser (zero-based)
Parser Documentation: Parser Documentation:

View File

@ -10,7 +10,7 @@ from jc import appdirs
from jc import utils from jc import utils
__version__ = '1.25.0' __version__ = '1.25.1'
parsers: List[str] = [ parsers: List[str] = [
'acpi', 'acpi',
@ -251,7 +251,8 @@ def _is_valid_parser_plugin(name: str, local_parsers_dir: str) -> bool:
else: else:
utils.warning_message([f'Not installing invalid parser plugin "{parser_mod_name}" at {local_parsers_dir}']) utils.warning_message([f'Not installing invalid parser plugin "{parser_mod_name}" at {local_parsers_dir}'])
return False return False
except Exception: except Exception as e:
utils.warning_message([f'Not installing parser plugin "{parser_mod_name}" at {local_parsers_dir} due to error: {e}'])
return False return False
return False return False
@ -324,7 +325,16 @@ def _get_parser(parser_mod_name: str) -> ModuleType:
parser_mod_name = _cliname_to_modname(parser_mod_name) parser_mod_name = _cliname_to_modname(parser_mod_name)
parser_cli_name = _modname_to_cliname(parser_mod_name) parser_cli_name = _modname_to_cliname(parser_mod_name)
modpath: str = 'jcparsers.' if parser_cli_name in local_parsers else 'jc.parsers.' modpath: str = 'jcparsers.' if parser_cli_name in local_parsers else 'jc.parsers.'
return importlib.import_module(f'{modpath}{parser_mod_name}') mod = None
try:
mod = importlib.import_module(f'{modpath}{parser_mod_name}')
except Exception as e:
mod = importlib.import_module(f'jc.parsers.disabled_parser')
mod.__name__ = parser_mod_name
utils.warning_message([f'"{parser_mod_name}" parser disabled due to error: {e}'])
return mod
def _parser_is_slurpable(parser: ModuleType) -> bool: def _parser_is_slurpable(parser: ModuleType) -> bool:
""" """

View File

@ -0,0 +1,23 @@
"""jc - JSON Convert broken parser - for testing purposes only"""
import non_existent_library
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = 'broken parser'
author = 'N/A'
author_email = 'N/A'
compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd']
hidden = True
__version__ = info.version
def parse(
data: str,
raw: bool = False,
quiet: bool = False
) -> dict:
"""Main text parsing function"""
return {}

View File

@ -0,0 +1,26 @@
"""jc - JSON Convert disabled parser
This parser has been disabled due to an error in the parser code.
"""
from jc.exceptions import ParseError
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = 'Disabled parser'
author = 'N/A'
author_email = 'N/A'
compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd']
hidden = True
__version__ = info.version
def parse(
data: str,
raw: bool = False,
quiet: bool = False
) -> dict:
"""Main text parsing function"""
raise ParseError('This parser is disabled.')

View File

@ -75,7 +75,7 @@ import uuid
class info(): class info():
"""Provides parser metadata (version, author, etc.)""" """Provides parser metadata (version, author, etc.)"""
version = '2.1' version = '2.2'
description = 'INI file parser' description = 'INI file parser'
author = 'Kelly Brazil' author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com' author_email = 'kellyjonbrazil@gmail.com'
@ -87,14 +87,10 @@ class info():
__version__ = info.version __version__ = info.version
class MyDict(dict): def _none_to_empty_string(data):
def __setitem__(self, key, value): if data is None:
# convert None values to empty string return ''
if value is None: return data
self[key] = ''
else:
super().__setitem__(key, value)
def _process(proc_data): def _process(proc_data):
@ -110,13 +106,18 @@ def _process(proc_data):
Dictionary representing the INI file. Dictionary representing the INI file.
""" """
# remove quotation marks from beginning and end of values # remove quotation marks from beginning and end of values
# and convert None to empty string
for k, v in proc_data.items(): for k, v in proc_data.items():
if isinstance(v, dict): if isinstance(v, dict):
for key, value in v.items(): for key, value in v.items():
v[key] = jc.utils.remove_quotes(value) value = _none_to_empty_string(value)
value = jc.utils.remove_quotes(value)
v[key] = value
continue continue
proc_data[k] = jc.utils.remove_quotes(v) v = _none_to_empty_string(v)
v = jc.utils.remove_quotes(v)
proc_data[k] = v
return proc_data return proc_data
@ -143,7 +144,6 @@ def parse(data, raw=False, quiet=False):
if jc.utils.has_data(data): if jc.utils.has_data(data):
ini_parser = configparser.ConfigParser( ini_parser = configparser.ConfigParser(
dict_type = MyDict,
allow_no_value=True, allow_no_value=True,
interpolation=None, interpolation=None,
default_section=None, default_section=None,
@ -175,4 +175,3 @@ def parse(data, raw=False, quiet=False):
raw_output.update(temp_dict) raw_output.update(temp_dict)
return raw_output if raw else _process(raw_output) return raw_output if raw else _process(raw_output)

View File

@ -44,6 +44,12 @@ Examples:
... ...
} }
""" """
import sys
# ugly hack because I accidentally shadowed the xml module from the
# standard library with the xml parser. :(
sys.path = [x for x in sys.path if 'jc/jc/parsers' not in x]
from typing import Dict, Union from typing import Dict, Union
import plistlib import plistlib
import binascii import binascii
@ -53,7 +59,7 @@ import jc.utils
class info(): class info():
"""Provides parser metadata (version, author, etc.)""" """Provides parser metadata (version, author, etc.)"""
version = '1.1' version = '1.2'
description = 'PLIST file parser' description = 'PLIST file parser'
author = 'Kelly Brazil' author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com' author_email = 'kellyjonbrazil@gmail.com'

View File

@ -8,6 +8,16 @@ from typing import ByteString
__all__ = ["Edid"] __all__ = ["Edid"]
# EDID:
# 00ffffffffffff004ca3523100000000
# 0014010380221378eac8959e57549226
# 0f505400000001010101010101010101
# 010101010101381d56d4500016303020
# 250058c2100000190000000f00000000
# 000000000025d9066a00000000fe0053
# 414d53554e470a204ca34154000000fe
# 004c544e313536415432343430310018
class Edid: class Edid:
"""Edid class """Edid class
@ -69,8 +79,10 @@ class Edid:
0b11: (16, 9), 0b11: (16, 9),
} }
_RawEdid = namedtuple("RawEdid", _RawEdid = namedtuple(
("header", "RawEdid",
(
"header",
"manu_id", "manu_id",
"prod_id", "prod_id",
"serial_no", "serial_no",
@ -92,7 +104,8 @@ class Edid:
"timing_3", "timing_3",
"timing_4", "timing_4",
"extension", "extension",
"checksum") "checksum",
),
) )
def __init__(self, edid: ByteString): def __init__(self, edid: ByteString):
@ -109,14 +122,16 @@ class Edid:
unpacked = struct.unpack(self._STRUCT_FORMAT, edid) unpacked = struct.unpack(self._STRUCT_FORMAT, edid)
raw_edid = self._RawEdid(*unpacked) raw_edid = self._RawEdid(*unpacked)
if raw_edid.header != b'\x00\xff\xff\xff\xff\xff\xff\x00': if raw_edid.header != b"\x00\xff\xff\xff\xff\xff\xff\x00":
raise ValueError("Invalid header.") raise ValueError("Invalid header.")
self.raw = edid self.raw = edid
self.manufacturer_id = raw_edid.manu_id self.manufacturer_id = raw_edid.manu_id
self.product = raw_edid.prod_id self.product = raw_edid.prod_id
self.year = raw_edid.manu_year + 1990 self.year = raw_edid.manu_year + 1990
self.edid_version = "{:d}.{:d}".format(raw_edid.edid_version, raw_edid.edid_revision) self.edid_version = "{:d}.{:d}".format(
raw_edid.edid_version, raw_edid.edid_revision
)
self.type = "digital" if (raw_edid.input_type & 0xFF) else "analog" self.type = "digital" if (raw_edid.input_type & 0xFF) else "analog"
self.width = float(raw_edid.width) self.width = float(raw_edid.width)
self.height = float(raw_edid.height) self.height = float(raw_edid.height)
@ -133,7 +148,7 @@ class Edid:
for i in range(8): for i in range(8):
bytes_data = raw_edid.timings_edid[2 * i : 2 * i + 2] bytes_data = raw_edid.timings_edid[2 * i : 2 * i + 2]
if bytes_data == b'\x01\x01': if bytes_data == b"\x01\x01":
continue continue
byte1, byte2 = bytes_data byte1, byte2 = bytes_data
x_res = 8 * (int(byte1) + 31) x_res = 8 * (int(byte1) + 31)
@ -145,9 +160,14 @@ class Edid:
self.name = None self.name = None
self.serial = None self.serial = None
for timing_bytes in (raw_edid.timing_1, raw_edid.timing_2, raw_edid.timing_3, raw_edid.timing_4): for timing_bytes in (
raw_edid.timing_1,
raw_edid.timing_2,
raw_edid.timing_3,
raw_edid.timing_4,
):
# "other" descriptor # "other" descriptor
if timing_bytes[0:2] == b'\x00\x00': if timing_bytes[0:2] == b"\x00\x00":
timing_type = timing_bytes[3] timing_type = timing_bytes[3]
if timing_type in (0xFF, 0xFE, 0xFC): if timing_type in (0xFF, 0xFE, 0xFC):
buffer = timing_bytes[5:] buffer = timing_bytes[5:]

View File

@ -65,7 +65,7 @@ import jc.utils
class info(): class info():
"""Provides parser metadata (version, author, etc.)""" """Provides parser metadata (version, author, etc.)"""
version = '1.8' version = '1.9'
description = '`uptime` command parser' description = '`uptime` command parser'
author = 'Kelly Brazil' author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com' author_email = 'kellyjonbrazil@gmail.com'
@ -160,10 +160,11 @@ def parse(data, raw=False, quiet=False):
jc.utils.input_type_check(data) jc.utils.input_type_check(data)
raw_output = {} raw_output = {}
cleandata = data.splitlines()
if jc.utils.has_data(data): if jc.utils.has_data(data):
time, _, *uptime, users, _, _, _, load_1m, load_5m, load_15m = cleandata[0].split() if 'users' in data:
# standard uptime output
time, _, *uptime, users, _, _, _, load_1m, load_5m, load_15m = data.split()
raw_output['time'] = time raw_output['time'] = time
raw_output['uptime'] = ' '.join(uptime).rstrip(',') raw_output['uptime'] = ' '.join(uptime).rstrip(',')
@ -172,7 +173,14 @@ def parse(data, raw=False, quiet=False):
raw_output['load_5m'] = load_5m.rstrip(',') raw_output['load_5m'] = load_5m.rstrip(',')
raw_output['load_15m'] = load_15m raw_output['load_15m'] = load_15m
if raw:
return raw_output
else: else:
return _process(raw_output) # users information missing (e.g. busybox)
time, _, *uptime, _, _, load_1m, load_5m, load_15m = data.split()
raw_output['time'] = time
raw_output['uptime'] = ' '.join(uptime).rstrip(',')
raw_output['load_1m'] = load_1m.rstrip(',')
raw_output['load_5m'] = load_5m.rstrip(',')
raw_output['load_15m'] = load_15m
return raw_output if raw else _process(raw_output)

View File

@ -73,15 +73,10 @@ Examples:
import jc.utils import jc.utils
from jc.exceptions import LibraryNotInstalled from jc.exceptions import LibraryNotInstalled
try:
import xmltodict
except Exception:
raise LibraryNotInstalled('The xmltodict library is not installed.')
class info(): class info():
"""Provides parser metadata (version, author, etc.)""" """Provides parser metadata (version, author, etc.)"""
version = '1.9' version = '1.10'
description = 'XML file parser' description = 'XML file parser'
author = 'Kelly Brazil' author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com' author_email = 'kellyjonbrazil@gmail.com'
@ -93,7 +88,7 @@ class info():
__version__ = info.version __version__ = info.version
def _process(proc_data, has_data=False): def _process(proc_data, has_data=False, xml_mod=None):
""" """
Final processing to conform to the schema. Final processing to conform to the schema.
@ -105,16 +100,19 @@ def _process(proc_data, has_data=False):
Dictionary representing an XML document. Dictionary representing an XML document.
""" """
if not xml_mod:
raise LibraryNotInstalled('The xmltodict library is not installed.')
proc_output = [] proc_output = []
if has_data: if has_data:
# standard output with @ prefix for attributes # standard output with @ prefix for attributes
try: try:
proc_output = xmltodict.parse(proc_data, proc_output = xml_mod.parse(proc_data,
dict_constructor=dict, dict_constructor=dict,
process_comments=True) process_comments=True)
except (ValueError, TypeError): except (ValueError, TypeError):
proc_output = xmltodict.parse(proc_data, dict_constructor=dict) proc_output = xml_mod.parse(proc_data, dict_constructor=dict)
return proc_output return proc_output
@ -133,6 +131,12 @@ def parse(data, raw=False, quiet=False):
Dictionary. Raw or processed structured data. Dictionary. Raw or processed structured data.
""" """
xmltodict = None
try:
import xmltodict
except Exception:
raise LibraryNotInstalled('The xmltodict library is not installed.')
jc.utils.compatibility(__name__, info.compatible, quiet) jc.utils.compatibility(__name__, info.compatible, quiet)
jc.utils.input_type_check(data) jc.utils.input_type_check(data)
@ -156,4 +160,4 @@ def parse(data, raw=False, quiet=False):
return raw_output return raw_output
return _process(data, has_data) return _process(data, has_data, xml_mod=xmltodict)

View File

@ -28,7 +28,7 @@ Schema:
"maximum_height": integer, "maximum_height": integer,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": integer, "resolution_width": integer,
"resolution_height": integer, "resolution_height": integer,
@ -77,7 +77,7 @@ Examples:
"maximum_height": 32767, "maximum_height": 32767,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": 1920, "resolution_width": 1920,
"resolution_height": 1080, "resolution_height": 1080,
@ -138,7 +138,7 @@ Examples:
"maximum_height": 32767, "maximum_height": 32767,
"devices": [ "devices": [
{ {
"modes": [ "resolution_modes": [
{ {
"resolution_width": 1920, "resolution_width": 1920,
"resolution_height": 1080, "resolution_height": 1080,
@ -189,16 +189,27 @@ Examples:
] ]
} }
""" """
from collections import defaultdict
from enum import Enum
import re import re
from typing import Dict, List, Optional, Union from typing import Dict, List, Tuple, Union
import jc.utils import jc.utils
from jc.parsers.pyedid.edid import Edid from jc.parsers.pyedid.edid import Edid
from jc.parsers.pyedid.helpers.edid_helper import EdidHelper from jc.parsers.pyedid.helpers.edid_helper import EdidHelper
Match = None
try:
# Added Python 3.7
Match = re.Match
except AttributeError:
Match = type(re.match("", ""))
class info: class info:
"""Provides parser metadata (version, author, etc.)""" """Provides parser metadata (version, author, etc.)"""
version = "1.4"
version = "2.0"
description = "`xrandr` command parser" description = "`xrandr` command parser"
author = "Kevin Lyter" author = "Kevin Lyter"
author_email = "code (at) lyterk.com" author_email = "code (at) lyterk.com"
@ -210,36 +221,10 @@ class info:
__version__ = info.version __version__ = info.version
# keep parsing state so we know which parsers have already tried the line # NOTE: When developing, comment out the try statement and catch block to get
# Structure is: # TypedDict type hints and valid type errors.
# {
# <line_string>: [
# <parser_string>
# ]
# }
#
# Where <line_string> is the xrandr output line to be checked and <parser_string>
# can contain "screen", "device", or "model"
parse_state: Dict[str, List] = {}
def _was_parsed(line: str, parser: str) -> bool:
"""
Check if entered parser has already parsed. If so return True.
If not, return false and add the parser to the list for the line entry.
"""
if line in parse_state:
if parser in parse_state[line]:
return True
parse_state[line].append(parser)
return False
parse_state[line] = [parser]
return False
try: try:
# Added in Python 3.8
from typing import TypedDict from typing import TypedDict
Frequency = TypedDict( Frequency = TypedDict(
@ -250,8 +235,8 @@ try:
"is_preferred": bool, "is_preferred": bool,
}, },
) )
Mode = TypedDict( ResolutionMode = TypedDict(
"Mode", "ResolutionMode",
{ {
"resolution_width": int, "resolution_width": int,
"resolution_height": int, "resolution_height": int,
@ -259,14 +244,15 @@ try:
"frequencies": List[Frequency], "frequencies": List[Frequency],
}, },
) )
Model = TypedDict( EdidModel = TypedDict(
"Model", "EdidModel",
{ {
"name": str, "name": str,
"product_id": str, "product_id": str,
"serial_number": str, "serial_number": str,
}, },
) )
Props = Dict[str, Union[List[str], EdidModel]]
Device = TypedDict( Device = TypedDict(
"Device", "Device",
{ {
@ -282,7 +268,8 @@ try:
"offset_height": int, "offset_height": int,
"dimension_width": int, "dimension_width": int,
"dimension_height": int, "dimension_height": int,
"modes": List[Mode], "props": Props,
"resolution_modes": List[ResolutionMode],
"rotation": str, "rotation": str,
"reflection": str, "reflection": str,
}, },
@ -307,12 +294,13 @@ try:
}, },
) )
except ImportError: except ImportError:
Screen = Dict[str, Union[int, str]] EdidModel = Dict[str, str]
Device = Dict[str, Union[str, int, bool]] Props = Dict[str, Union[List[str], EdidModel]]
Frequency = Dict[str, Union[float, bool]] Frequency = Dict[str, Union[float, bool]]
Mode = Dict[str, Union[int, bool, List[Frequency]]] ResolutionMode = Dict[str, Union[int, bool, List[Frequency]]]
Model = Dict[str, str] Device = Dict[str, Union[str, int, bool, List[ResolutionMode]]]
Response = Dict[str, Union[Device, Mode, Screen]] Screen = Dict[str, Union[int, List[Device]]]
Response = Dict[str, Screen]
_screen_pattern = ( _screen_pattern = (
@ -323,33 +311,6 @@ _screen_pattern = (
) )
def _parse_screen(next_lines: List[str]) -> Optional[Screen]:
next_line = next_lines.pop()
if _was_parsed(next_line, 'screen'):
return None
result = re.match(_screen_pattern, next_line)
if not result:
next_lines.append(next_line)
return None
raw_matches = result.groupdict()
screen: Screen = {"devices": []}
for k, v in raw_matches.items():
screen[k] = int(v)
while next_lines:
device: Optional[Device] = _parse_device(next_lines)
if not device:
break
else:
screen["devices"].append(device)
return screen
# eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) # eDP1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis)
# 310mm x 170mm # 310mm x 170mm
# regex101 demo link # regex101 demo link
@ -365,25 +326,106 @@ _device_pattern = (
+ r"( ?((?P<dimension_width>\d+)mm x (?P<dimension_height>\d+)mm)?)?" + r"( ?((?P<dimension_width>\d+)mm x (?P<dimension_height>\d+)mm)?)?"
) )
# 1920x1080i 60.03*+ 59.93
# 1920x1080 60.00 + 50.00 59.94
_resolution_mode_pattern = r"\s*(?P<resolution_width>\d+)x(?P<resolution_height>\d+)(?P<is_high_resolution>i)?\s+(?P<rest>.*)"
_frequencies_pattern = r"(((?P<frequency>\d+\.\d+)(?P<star>\*| |)(?P<plus>\+?)?)+)"
def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device]:
if not next_lines:
return None
next_line = next_lines.pop() # Values sometimes appear on the same lines as the keys (CscMatrix), sometimes on the line
# below (as with EDIDs), and sometimes both (CTM).
# Capture the key line that way.
#
# CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
# 0 1
# CscMatrix: 65536 0 0 0 0 65536 0 0 0 0 65536 0
# EDID:
# 00ffffffffffff0010ac33424c303541
# 0f210104b53c22783eee95a3544c9926
_prop_key_pattern = r"\s+(?P<key>[\w| |\-|_]+):\s?(?P<maybe_value>.*)"
if _was_parsed(next_line, 'device'):
return None
result = re.match(_device_pattern, next_line) class LineType(Enum):
if not result: Screen = 1
next_lines.append(next_line) Device = 2
return None ResolutionMode = 3
PropKey = 4
PropValue = 5
Invalid = 6
matches = result.groupdict()
class _Line:
"""Provide metadata about line to make handling it more simple across fn boundaries"""
def __init__(self, s: str, t: LineType, m: Match):
self.s = s
self.t = t
self.m = m
@classmethod
def categorize(cls, line: str) -> "_Line":
"""Iterate through line char by char to see what type of line it is. Apply regexes for more distinctness. Save the regexes and return them for later processing."""
i = 0
tab_count = 0
while True:
try:
c = line[i]
except:
# Really shouldn't be getting to the end of the line
raise Exception(f"Reached end of line unexpectedly: '{line}'")
if not c.isspace():
if tab_count == 0:
screen_match = re.match(_screen_pattern, line)
if screen_match:
return cls(line, LineType.Screen, screen_match)
device_match = re.match(_device_pattern, line)
if device_match:
return cls(line, LineType.Device, device_match)
else:
break
elif tab_count == 1:
match = re.match(_prop_key_pattern, line)
if match:
return cls(line, LineType.PropKey, match)
else:
break
else:
match = re.match(r"\s+(.*)\s+", line)
if match:
return cls(line, LineType.PropValue, match)
else:
break
else:
if c == " ":
match = re.match(_resolution_mode_pattern, line)
if match:
return cls(line, LineType.ResolutionMode, match)
else:
break
elif c == "\t":
tab_count += 1
i += 1
raise Exception(f"Line could not be categorized: '{line}'")
def _parse_screen(line: _Line) -> Screen:
d = line.m.groupdict()
screen: Screen = {"devices": []} # type: ignore # Will be populated, but not immediately.
for k, v in d.items():
screen[k] = int(v)
return screen
def _parse_device(line: _Line) -> Device:
matches = line.m.groupdict()
device: Device = { device: Device = {
"modes": [], "props": defaultdict(list),
"resolution_modes": [],
"is_connected": matches["is_connected"] == "connected", "is_connected": matches["is_connected"] == "connected",
"is_primary": matches["is_primary"] is not None "is_primary": matches["is_primary"] is not None
and len(matches["is_primary"]) > 0, and len(matches["is_primary"]) > 0,
@ -403,97 +445,20 @@ def _parse_device(next_lines: List[str], quiet: bool = False) -> Optional[Device
if v: if v:
device[k] = int(v) device[k] = int(v)
except ValueError: except ValueError:
if not quiet: raise Exception([f"{line.s} : {k} - {v} is not int-able"])
jc.utils.warning_message(
[f"{next_line} : {k} - {v} is not int-able"]
)
model: Optional[Model] = _parse_model(next_lines, quiet)
if model:
device["model_name"] = model["name"]
device["product_id"] = model["product_id"]
device["serial_number"] = model["serial_number"]
while next_lines:
next_line = next_lines.pop()
next_mode: Optional[Mode] = _parse_mode(next_line)
if next_mode:
device["modes"].append(next_mode)
else:
if re.match(_device_pattern, next_line):
next_lines.append(next_line)
break
return device return device
# EDID: def _parse_resolution_mode(line: _Line) -> ResolutionMode:
# 00ffffffffffff004ca3523100000000
# 0014010380221378eac8959e57549226
# 0f505400000001010101010101010101
# 010101010101381d56d4500016303020
# 250058c2100000190000000f00000000
# 000000000025d9066a00000000fe0053
# 414d53554e470a204ca34154000000fe
# 004c544e313536415432343430310018
_edid_head_pattern = r"\s*EDID:\s*"
_edid_line_pattern = r"\s*(?P<edid_line>[0-9a-fA-F]{32})\s*"
def _parse_model(next_lines: List[str], quiet: bool = False) -> Optional[Model]:
if not next_lines:
return None
next_line = next_lines.pop()
if _was_parsed(next_line, 'model'):
return None
if not re.match(_edid_head_pattern, next_line):
next_lines.append(next_line)
return None
edid_hex_value = ""
while next_lines:
next_line = next_lines.pop()
result = re.match(_edid_line_pattern, next_line)
if not result:
next_lines.append(next_line)
break
matches = result.groupdict()
edid_hex_value += matches["edid_line"]
edid = Edid(EdidHelper.hex2bytes(edid_hex_value))
model: Model = {
"name": edid.name or "Generic",
"product_id": str(edid.product),
"serial_number": str(edid.serial),
}
return model
# 1920x1080i 60.03*+ 59.93
# 1920x1080 60.00 + 50.00 59.94
_mode_pattern = r"\s*(?P<resolution_width>\d+)x(?P<resolution_height>\d+)(?P<is_high_resolution>i)?\s+(?P<rest>.*)"
_frequencies_pattern = r"(((?P<frequency>\d+\.\d+)(?P<star>\*| |)(?P<plus>\+?)?)+)"
def _parse_mode(line: str) -> Optional[Mode]:
result = re.match(_mode_pattern, line)
frequencies: List[Frequency] = [] frequencies: List[Frequency] = []
if not result: d = line.m.groupdict()
return None
d = result.groupdict()
resolution_width = int(d["resolution_width"]) resolution_width = int(d["resolution_width"])
resolution_height = int(d["resolution_height"]) resolution_height = int(d["resolution_height"])
is_high_resolution = d["is_high_resolution"] is not None is_high_resolution = d["is_high_resolution"] is not None
mode: Mode = { mode: ResolutionMode = {
"resolution_width": resolution_width, "resolution_width": resolution_width,
"resolution_height": resolution_height, "resolution_height": resolution_height,
"is_high_resolution": is_high_resolution, "is_high_resolution": is_high_resolution,
@ -518,7 +483,45 @@ def _parse_mode(line: str) -> Optional[Mode]:
return mode return mode
def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict: def _parse_props(index: int, line: _Line, lines: List[str]) -> Tuple[int, Props]:
tmp_props: Dict[str, List[str]] = {}
key = ""
while index <= len(lines):
if line.t == LineType.PropKey:
d = line.m.groupdict()
# See _prop_key_pattern
key = d["key"]
maybe_value = d["maybe_value"]
if not maybe_value:
tmp_props[key] = []
else:
tmp_props[key] = [maybe_value]
elif line.t == LineType.PropValue:
tmp_props[key].append(line.s.strip())
else:
# We've gone past our props and need to ascend
index = index - 1
break
index += 1
try:
line = _Line.categorize(lines[index])
except:
pass
props: Props = {}
if "EDID" in tmp_props:
edid = Edid(EdidHelper.hex2bytes("".join(tmp_props["EDID"])))
model: EdidModel = {
"name": edid.name or "Generic",
"product_id": str(edid.product),
"serial_number": str(edid.serial),
}
props["EdidModel"] = model
return index, {**tmp_props, **props}
def parse(data: str, raw: bool = False, quiet: bool = False) -> Response:
""" """
Main text parsing function Main text parsing function
@ -535,15 +538,34 @@ def parse(data: str, raw: bool = False, quiet: bool = False) -> Dict:
jc.utils.compatibility(__name__, info.compatible, quiet) jc.utils.compatibility(__name__, info.compatible, quiet)
jc.utils.input_type_check(data) jc.utils.input_type_check(data)
linedata = data.splitlines() index = 0
linedata.reverse() # For popping lines = data.splitlines()
result: Dict = {} screen, device = None, None
result: Response = {"screens": []}
if jc.utils.has_data(data): if jc.utils.has_data(data):
result = {"screens": []} while index < len(lines):
while linedata: line = _Line.categorize(lines[index])
screen = _parse_screen(linedata) if line.t == LineType.Screen:
if screen: screen = _parse_screen(line)
result["screens"].append(screen) result["screens"].append(screen)
elif line.t == LineType.Device:
device = _parse_device(line)
if not screen:
raise Exception("There should be an identifiable screen")
screen["devices"].append(device)
elif line.t == LineType.ResolutionMode:
resolution_mode = _parse_resolution_mode(line)
if not device:
raise Exception("Undefined device")
device["resolution_modes"].append(resolution_mode)
elif line.t == LineType.PropKey:
# Props needs to be state aware, it owns the index.
ix, props = _parse_props(index, line, lines)
index = ix
if not device:
raise Exception("Undefined device")
device["props"] = props
index += 1
return result return result

View File

@ -12,6 +12,7 @@ from functools import lru_cache
from typing import Any, List, Dict, Iterable, Union, Optional, TextIO from typing import Any, List, Dict, Iterable, Union, Optional, TextIO
from .jc_types import TimeStampFormatType from .jc_types import TimeStampFormatType
CLI_QUIET = False
def _asciify(string: str) -> str: def _asciify(string: str) -> str:
""" """
@ -62,6 +63,9 @@ def warning_message(message_lines: List[str]) -> None:
None - just prints output to STDERR None - just prints output to STDERR
""" """
if CLI_QUIET:
return
# this is for backwards compatibility with existing custom parsers # this is for backwards compatibility with existing custom parsers
if isinstance(message_lines, str): if isinstance(message_lines, str):
message_lines = [message_lines] message_lines = [message_lines]

View File

@ -1,4 +1,4 @@
.TH jc 1 2024-02-05 1.25.0 "JSON Convert" .TH jc 1 2024-02-12 1.25.1 "JSON Convert"
.SH NAME .SH NAME
\fBjc\fP \- JSON Convert JSONifies the output of many CLI tools, file-types, \fBjc\fP \- JSON Convert JSONifies the output of many CLI tools, file-types,
and strings and strings

7
runtests-missing-libs.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
# system should be in "America/Los_Angeles" timezone for all tests to pass
# ensure no local plugin parsers are installed for all tests to pass
pip uninstall pygments ruamel.yaml xmltodict --yes
python3 -m unittest -v
pip install pygments ruamel.yaml xmltodict

View File

@ -5,7 +5,7 @@ with open('README.md', 'r') as f:
setuptools.setup( setuptools.setup(
name='jc', name='jc',
version='1.25.0', version='1.25.1',
author='Kelly Brazil', author='Kelly Brazil',
author_email='kellyjonbrazil@gmail.com', author_email='kellyjonbrazil@gmail.com',
description='Converts the output of popular command-line tools and file-types to JSON.', description='Converts the output of popular command-line tools and file-types to JSON.',

View File

@ -0,0 +1 @@
{"server":{},"mysqld":{"user":["mysql"],"pid_file":["/var/run/mysqld/mysqld.pid"],"port":["3306"],"basedir":["/usr"],"datadir":["/var/lib/mysql"],"tmpdir":["/tmp"],"lc_messages_dir":["/usr/share/mysql"],"skip_external_locking":[""],"bind_address":["127.0.0.1"],"key_buffer_size":["16M"],"max_allowed_packet":["64M"],"thread_stack":["192K"],"thread_cache_size":["8"],"myisam_recover_options":["BACKUP"],"max_connections":["80"],"max_user_connections":["0"],"table_cache":["64"],"thread_concurrency":["10"],"open_files_limit":["122880"],"table_open_cache":["6000"],"tmp_table_size":["32M"],"join_buffer_size":["8M"],"max_heap_table_size":["32M"],"query_cache_type":["0"],"query_cache_limit":["0"],"query_cache_size":["0"],"log_error":["/var/log/mysql/error.log"],"expire_logs_days":["10"],"max_binlog_size":["100M"],"innodb_buffer_pool_size":["1G"],"innodb_log_file_size":["256M"],"character_set_server":["utf8mb4"],"collation_server":["utf8mb4_general_ci"],"ignore_db_dir":["lost+found"]},"embedded":{},"mariadb":{"performance_schema":["ON"],"performance_schema_instrument":["stage/%=ON"],"performance_schema_consumer_events_stages_current":["ON"],"performance_schema_consumer_events_stages_history":["ON"],"performance_schema_consumer_events_stages_history_long":["ON"]},"mariadb-10.1":{}}

151
tests/fixtures/generic/ini-mariadb.ini vendored Normal file
View File

@ -0,0 +1,151 @@
# Ansible managed
# These groups are read by MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql/
#
# this is read by the standalone daemon and embedded servers
[server]
# this is only for the mysqld standalone daemon
[mysqld]
#
# * Basic Settings
#
user = mysql
pid_file = /var/run/mysqld/mysqld.pid
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc_messages_dir = /usr/share/mysql
skip_external_locking
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind_address = 127.0.0.1
#
# * Fine Tuning
#
key_buffer_size = 16M
max_allowed_packet = 64M
thread_stack = 192K
thread_cache_size = 8
# This replaces the startup script and checks MyISAM tables if needed
# the first time they are touched
myisam_recover_options = BACKUP
max_connections = 80
max_user_connections = 0
table_cache = 64
thread_concurrency = 10
open_files_limit = 122880
table_open_cache = 6000
tmp_table_size = 32M
join_buffer_size = 8M
max_heap_table_size = 32M
#
# * Query Cache Configuration
#
# Disabled by default in MariaDB >= 10.1.7 see:
# https://mariadb.com/kb/en/query-cache/
query_cache_type = 0
query_cache_limit = 0
query_cache_size = 0
#
# * Logging and Replication
#
# Both location gets rotated by the cronjob.
# Be aware that this log type is a performance killer.
# As of 5.1 you can enable the log at runtime!
#general_log_file = /var/log/mysql/mysql.log
#general_log = 1
#
# Error log - should be very few entries.
#
log_error = /var/log/mysql/error.log
#
# Enable the slow query log to see queries with especially long duration
#slow_query_log_file = /var/log/mysql/mariadb-slow.log
#long_query_time = 10
#log_slow_rate_limit = 1000
#log_slow_verbosity = query_plan
#log-queries-not-using-indexes
#
# The following can be used as easy to replay backup logs or for replication.
# note: if you are setting up a replication slave, see README.Debian about
# other settings you may need to change.
#server-id = 1
#log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = exclude_database_name
#
# * InnoDB
#
# InnoDB is enabled by default with a 10MB datafile in /var/lib/mysql/.
# Read the manual for more InnoDB related options. There are many!
innodb_buffer_pool_size = 1G
innodb_log_file_size = 256M
# * Security Features
#
# Read the manual, too, if you want chroot!
# chroot = /var/lib/mysql/
#
# For generating SSL certificates you can use for example the GUI tool "tinyca".
#
# ssl-ca=/etc/mysql/cacert.pem
# ssl-cert=/etc/mysql/server-cert.pem
# ssl-key=/etc/mysql/server-key.pem
#
# Accept only connections using the latest and most secure TLS protocol version.
# ..when MariaDB is compiled with OpenSSL:
# ssl-cipher=TLSv1.2
# ..when MariaDB is compiled with YaSSL (default in Debian):
# ssl=on
#
# * Character sets
#
# MySQL/MariaDB default is Latin1, but in Debian we rather default to the full
# utf8 4-byte character set. See also client.cnf
#
character_set_server = utf8mb4
collation_server = utf8mb4_general_ci
ignore_db_dir = lost+found
#
# * Unix socket authentication plugin is built-in since 10.0.22-6
#
# Needed so the root database user can authenticate without a password but
# only when running as the unix root user.
#
# Also available for other users if required.
# See https://mariadb.com/kb/en/unix_socket-authentication-plugin/
# this is only for embedded server
[embedded]
# This group is only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]
# https://mariadb.com/kb/en/library/performance-schema-overview/
performance_schema=ON
performance_schema_instrument='stage/%=ON'
performance_schema_consumer_events_stages_current=ON
performance_schema_consumer_events_stages_history=ON
performance_schema_consumer_events_stages_history_long=ON
# This group is only read by MariaDB-10.1 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand
[mariadb-10.1]
# vim: syntax=dosini

View File

@ -0,0 +1 @@
{"server":{},"mysqld":{"user":"mysql","pid_file":"/var/run/mysqld/mysqld.pid","port":"3306","basedir":"/usr","datadir":"/var/lib/mysql","tmpdir":"/tmp","lc_messages_dir":"/usr/share/mysql","skip_external_locking":"","bind_address":"127.0.0.1","key_buffer_size":"16M","max_allowed_packet":"64M","thread_stack":"192K","thread_cache_size":"8","myisam_recover_options":"BACKUP","max_connections":"80","max_user_connections":"0","table_cache":"64","thread_concurrency":"10","open_files_limit":"122880","table_open_cache":"6000","tmp_table_size":"32M","join_buffer_size":"8M","max_heap_table_size":"32M","query_cache_type":"0","query_cache_limit":"0","query_cache_size":"0","log_error":"/var/log/mysql/error.log","expire_logs_days":"10","max_binlog_size":"100M","innodb_buffer_pool_size":"1G","innodb_log_file_size":"256M","character_set_server":"utf8mb4","collation_server":"utf8mb4_general_ci","ignore_db_dir":"lost+found"},"embedded":{},"mariadb":{"performance_schema":"ON","performance_schema_instrument":"stage/%=ON","performance_schema_consumer_events_stages_current":"ON","performance_schema_consumer_events_stages_history":"ON","performance_schema_consumer_events_stages_history_long":"ON"},"mariadb-10.1":{}}

View File

@ -0,0 +1,138 @@
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
EDID:
00ffffffffffff0006af3d5700000000
001c0104a51f1178022285a5544d9a27
0e505400000001010101010101010101
010101010101b43780a070383e401010
350035ae100000180000000f00000000
00000000000000000020000000fe0041
554f0a202020202020202020000000fe
004231343048414e30352e37200a0070
scaling mode: Full aspect
supported: Full, Center, Full aspect
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
panel orientation: Normal
supported: Normal, Upside Down, Left Side Up, Right Side Up
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 95
supported: 95
non-desktop: 0
range: (0, 1)
1920x1080 60.03*+ 60.01 59.97 59.96 59.93
1680x1050 59.95 59.88
1400x1050 59.98
1600x900 59.99 59.94 59.95 59.82
1280x1024 60.02
1400x900 59.96 59.88
1280x960 60.00
1440x810 60.00 59.97
1368x768 59.88 59.85
1280x800 59.99 59.97 59.81 59.91
1280x720 60.00 59.99 59.86 59.74
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
1024x576 59.95 59.96 59.90 59.82
960x600 59.93 60.00
960x540 59.96 59.99 59.63 59.82
800x600 60.00 60.32 56.25
840x525 60.01 59.88
864x486 59.92 59.57
700x525 59.98
800x450 59.95 59.82
640x512 60.02
700x450 59.96 59.88
640x480 60.00 59.94
720x405 59.51 58.99
684x384 59.88 59.85
640x400 59.88 59.98
640x360 59.86 59.83 59.84 59.32
512x384 60.00
512x288 60.00 59.92
480x270 59.63 59.82
400x300 60.32 56.34
432x243 59.92 59.57
320x240 60.05
360x202 59.51 59.13
320x180 59.84 59.32
DP-1 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 103
supported: 103
non-desktop: 0
range: (0, 1)
HDMI-1 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
max bpc: 12
range: (8, 12)
content type: No Data
supported: No Data, Graphics, Photo, Cinema, Game
Colorspace: Default
supported: Default, SMPTE_170M_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, DCI-P3_RGB_Theater
aspect ratio: Automatic
supported: Automatic, 4:3, 16:9
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 113
supported: 113
non-desktop: 0
range: (0, 1)
DP-2 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 119
supported: 119
non-desktop: 0
range: (0, 1)

View File

@ -0,0 +1,138 @@
Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384
eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm
EDID:
00ffffffffffff0006af3d5700000000
001c0104a51f1178022285a5544d9a27
0e505400000001010101010101010101
010101010101b43780a070383e401010
350035ae100000180000000f00000000
00000000000000000020000000fe0041
554f0a202020202020202020000000fe
004231343048414e30352e37200a0070
scaling mode: Full aspect
supported: Full, Center, Full aspect
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
panel orientation: Normal
supported: Normal, Upside Down, Left Side Up, Right Side Up
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 95
supported: 95
non-desktop: 0
range: (0, 1)
1920x1080 60.03*+ 60.01 59.97 59.96 59.93
1680x1050 59.95 59.88
1400x1050 59.98
1600x900 59.99 59.94 59.95 59.82
1280x1024 60.02
1400x900 59.96 59.88
1280x960 60.00
1440x810 60.00 59.97
1368x768 59.88 59.85
1280x800 59.99 59.97 59.81 59.91
1280x720 60.00 59.99 59.86 59.74
1024x768 60.04 60.00
960x720 60.00
928x696 60.05
896x672 60.01
1024x576 59.95 59.96 59.90 59.82
960x600 59.93 60.00
960x540 59.96 59.99 59.63 59.82
800x600 60.00 60.32 56.25
840x525 60.01 59.88
864x486 59.92 59.57
700x525 59.98
800x450 59.95 59.82
640x512 60.02
700x450 59.96 59.88
640x480 60.00 59.94
720x405 59.51 58.99
684x384 59.88 59.85
640x400 59.88 59.98
640x360 59.86 59.83 59.84 59.32
512x384 60.00
512x288 60.00 59.92
480x270 59.63 59.82
400x300 60.32 56.34
432x243 59.92 59.57
320x240 60.05
360x202 59.51 59.13
320x180 59.84 59.32
DP-1 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 103
supported: 103
non-desktop: 0
range: (0, 1)
HDMI-1 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
max bpc: 12
range: (8, 12)
content type: No Data
supported: No Data, Graphics, Photo, Cinema, Game
Colorspace: Default
supported: Default, SMPTE_170M_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, opRGB, BT2020_CYCC, BT2020_RGB, BT2020_YCC, DCI-P3_RGB_D65, DCI-P3_RGB_Theater
aspect ratio: Automatic
supported: Automatic, 4:3, 16:9
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 113
supported: 113
non-desktop: 0
range: (0, 1)
DP-2 disconnected (normal left inverted right x axis y axis)
HDCP Content Type: HDCP Type0
supported: HDCP Type0, HDCP Type1
Content Protection: Undesired
supported: Undesired, Desired, Enabled
Colorspace: Default
supported: Default, RGB_Wide_Gamut_Fixed_Point, RGB_Wide_Gamut_Floating_Point, opRGB, DCI-P3_RGB_D65, BT2020_RGB, BT601_YCC, BT709_YCC, XVYCC_601, XVYCC_709, SYCC_601, opYCC_601, BT2020_CYCC, BT2020_YCC
max bpc: 12
range: (6, 12)
Broadcast RGB: Automatic
supported: Automatic, Full, Limited 16:235
audio: auto
supported: force-dvi, off, auto, on
subconnector: Unknown
supported: Unknown, VGA, DVI-D, HDMI, DP, Wireless, Native
link-status: Good
supported: Good, Bad
CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0
0 1
CONNECTOR_ID: 119
supported: 119
non-desktop: 0
range: (0, 1)

View File

@ -21,6 +21,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.ini'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.ini'), 'r', encoding='utf-8') as f:
generic_ini_single_quote = f.read() generic_ini_single_quote = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-mariadb.ini'), 'r', encoding='utf-8') as f:
generic_ini_mariadb = f.read()
# output # output
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-test.json'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-test.json'), 'r', encoding='utf-8') as f:
generic_ini_test_json = json.loads(f.read()) generic_ini_test_json = json.loads(f.read())
@ -34,6 +37,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.json'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.json'), 'r', encoding='utf-8') as f:
generic_ini_single_quote_json = json.loads(f.read()) generic_ini_single_quote_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-mariadb.json'), 'r', encoding='utf-8') as f:
generic_ini_mariadb_json = json.loads(f.read())
def test_ini_nodata(self): def test_ini_nodata(self):
""" """
@ -53,6 +59,12 @@ class MyTests(unittest.TestCase):
""" """
self.assertEqual(jc.parsers.ini.parse(self.generic_ini_iptelserver, quiet=True), self.generic_ini_iptelserver_json) self.assertEqual(jc.parsers.ini.parse(self.generic_ini_iptelserver, quiet=True), self.generic_ini_iptelserver_json)
def test_ini_mariadb(self):
"""
Test the mariadb ini file
"""
self.assertEqual(jc.parsers.ini.parse(self.generic_ini_mariadb, quiet=True), self.generic_ini_mariadb_json)
def test_ini_duplicate_keys(self): def test_ini_duplicate_keys(self):
""" """
Test input that contains duplicate keys. Only the last value should be used. Test input that contains duplicate keys. Only the last value should be used.
@ -104,6 +116,15 @@ key5 = "quoted"
""" """
self.assertEqual(jc.parsers.ini.parse(self.generic_ini_single_quote, quiet=True), self.generic_ini_single_quote_json) self.assertEqual(jc.parsers.ini.parse(self.generic_ini_single_quote, quiet=True), self.generic_ini_single_quote_json)
def test_ini_single_key_no_value(self):
"""
Test ini file with a single item with no value. This caused issues in jc v.1.25.0
"""
data = '''[data]
novalue
'''
expected = {"data":{"novalue":""}}
self.assertEqual(jc.parsers.ini.parse(data, quiet=True), expected)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -21,6 +21,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.ini'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-single-quote.ini'), 'r', encoding='utf-8') as f:
generic_ini_single_quote = f.read() generic_ini_single_quote = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-mariadb.ini'), 'r', encoding='utf-8') as f:
generic_ini_mariadb = f.read()
# output # output
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-dup-test.json'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-dup-test.json'), 'r', encoding='utf-8') as f:
generic_ini_dup_test_json = json.loads(f.read()) generic_ini_dup_test_json = json.loads(f.read())
@ -34,6 +37,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-dup-single-quote.json'), 'r', encoding='utf-8') as f: with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-dup-single-quote.json'), 'r', encoding='utf-8') as f:
generic_ini_dup_single_quote_json = json.loads(f.read()) generic_ini_dup_single_quote_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ini-dup-mariadb.json'), 'r', encoding='utf-8') as f:
generic_ini_dup_mariadb_json = json.loads(f.read())
def test_ini_dup_nodata(self): def test_ini_dup_nodata(self):
""" """
@ -53,6 +59,12 @@ class MyTests(unittest.TestCase):
""" """
self.assertEqual(jc.parsers.ini_dup.parse(self.generic_ini_iptelserver, quiet=True), self.generic_ini_dup_iptelserver_json) self.assertEqual(jc.parsers.ini_dup.parse(self.generic_ini_iptelserver, quiet=True), self.generic_ini_dup_iptelserver_json)
def test_ini_dup_mariadb(self):
"""
Test the mariadb ini file
"""
self.assertEqual(jc.parsers.ini_dup.parse(self.generic_ini_mariadb, quiet=True), self.generic_ini_dup_mariadb_json)
def test_ini_dup_duplicate_keys(self): def test_ini_dup_duplicate_keys(self):
""" """
Test input that contains duplicate keys. Test input that contains duplicate keys.
@ -94,7 +106,15 @@ key5 = "quoted"
""" """
self.assertEqual(jc.parsers.ini_dup.parse(self.generic_ini_single_quote, quiet=True), self.generic_ini_dup_single_quote_json) self.assertEqual(jc.parsers.ini_dup.parse(self.generic_ini_single_quote, quiet=True), self.generic_ini_dup_single_quote_json)
def test_ini_dup_single_key_no_value(self):
"""
Test ini file with a single item with no value.
"""
data = '''[data]
novalue
'''
expected = {"data":{"novalue":[""]}}
self.assertEqual(jc.parsers.ini_dup.parse(data, quiet=True), expected)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,12 +1,20 @@
import os import os
import unittest import unittest
from datetime import datetime, timezone from datetime import datetime, timezone
try: try:
import pygments import pygments
from pygments.token import (Name, Number, String, Keyword) from pygments.token import (Name, Number, String, Keyword)
PYGMENTS_INSTALLED=True PYGMENTS_INSTALLED=True
except ModuleNotFoundError: except:
PYGMENTS_INSTALLED=False PYGMENTS_INSTALLED=False
try:
import ruamel.yaml
RUAMELYAML_INSTALLED = True
except:
RUAMELYAML_INSTALLED = False
from jc.cli import JcCli from jc.cli import JcCli
import jc.parsers.url as url_parser import jc.parsers.url as url_parser
import jc.parsers.proc as proc_parser import jc.parsers.proc as proc_parser
@ -47,8 +55,8 @@ class MyTests(unittest.TestCase):
resulting_attributes = (cli.magic_found_parser, cli.magic_options, cli.magic_run_command) resulting_attributes = (cli.magic_found_parser, cli.magic_options, cli.magic_run_command)
self.assertEqual(expected, resulting_attributes) self.assertEqual(expected, resulting_attributes)
@unittest.skipIf(not PYGMENTS_INSTALLED, 'pygments library not installed')
def test_cli_set_env_colors(self): def test_cli_set_env_colors(self):
if PYGMENTS_INSTALLED:
if pygments.__version__.startswith('2.3.'): if pygments.__version__.startswith('2.3.'):
env = { env = {
'': { '': {
@ -146,8 +154,8 @@ class MyTests(unittest.TestCase):
cli.set_custom_colors() cli.set_custom_colors()
self.assertEqual(cli.custom_colors, expected_colors) self.assertEqual(cli.custom_colors, expected_colors)
@unittest.skipIf(not PYGMENTS_INSTALLED, 'pygments library not installed')
def test_cli_json_out(self): def test_cli_json_out(self):
if PYGMENTS_INSTALLED:
test_input = [ test_input = [
None, None,
{}, {},
@ -180,8 +188,8 @@ class MyTests(unittest.TestCase):
cli.data_out = test_dict cli.data_out = test_dict
self.assertEqual(cli.json_out(), expected_json) self.assertEqual(cli.json_out(), expected_json)
@unittest.skipIf(not PYGMENTS_INSTALLED, 'pygments library not installed')
def test_cli_json_out_mono(self): def test_cli_json_out_mono(self):
if PYGMENTS_INSTALLED:
test_input = [ test_input = [
None, None,
{}, {},
@ -205,6 +213,7 @@ class MyTests(unittest.TestCase):
cli.data_out = test_dict cli.data_out = test_dict
self.assertEqual(cli.json_out(), expected_json) self.assertEqual(cli.json_out(), expected_json)
@unittest.skipIf(not PYGMENTS_INSTALLED, 'pygments library not installed')
def test_cli_json_out_pretty(self): def test_cli_json_out_pretty(self):
test_input = [ test_input = [
{"key1": "value1", "key2": 2, "key3": None, "key4": 3.14, "key5": True}, {"key1": "value1", "key2": 2, "key3": None, "key4": 3.14, "key5": True},
@ -229,8 +238,27 @@ class MyTests(unittest.TestCase):
cli.data_out = test_dict cli.data_out = test_dict
self.assertEqual(cli.json_out(), expected_json) self.assertEqual(cli.json_out(), expected_json)
@unittest.skipIf(PYGMENTS_INSTALLED, 'pygments library installed')
def test_cli_json_out_pretty_no_pygments(self):
test_input = [
{"key1": "value1", "key2": 2, "key3": None, "key4": 3.14, "key5": True},
{"key1": [{"subkey1": "subvalue1"}, {"subkey2": [1, 2, 3]}], "key2": True}
]
expected_output = [
'{\n "key1": "value1",\n "key2": 2,\n "key3": null,\n "key4": 3.14,\n "key5": true\n}',
'{\n "key1": [\n {\n "subkey1": "subvalue1"\n },\n {\n "subkey2": [\n 1,\n 2,\n 3\n ]\n }\n ],\n "key2": true\n}'
]
for test_dict, expected_json in zip(test_input, expected_output):
cli = JcCli()
cli.pretty = True
cli.set_custom_colors()
cli.data_out = test_dict
self.assertEqual(cli.json_out(), expected_json)
@unittest.skipIf(not PYGMENTS_INSTALLED, 'pygments library not installed')
def test_cli_yaml_out(self): def test_cli_yaml_out(self):
if PYGMENTS_INSTALLED:
test_input = [ test_input = [
None, None,
{}, {},
@ -263,6 +291,7 @@ class MyTests(unittest.TestCase):
cli.data_out = test_dict cli.data_out = test_dict
self.assertEqual(cli.yaml_out(), expected_json) self.assertEqual(cli.yaml_out(), expected_json)
@unittest.skipIf(not RUAMELYAML_INSTALLED, 'ruamel.yaml library not installed')
def test_cli_yaml_out_mono(self): def test_cli_yaml_out_mono(self):
test_input = [ test_input = [
None, None,
@ -295,6 +324,10 @@ class MyTests(unittest.TestCase):
self.assertGreaterEqual(cli.about_jc()['parser_count'], 55) self.assertGreaterEqual(cli.about_jc()['parser_count'], 55)
self.assertEqual(cli.about_jc()['parser_count'], len(cli.about_jc()['parsers'])) self.assertEqual(cli.about_jc()['parser_count'], len(cli.about_jc()['parsers']))
def test_cli_parsers_text(self):
cli = JcCli()
self.assertIsNot(cli.parsers_text, '')
def test_add_meta_to_simple_dict(self): def test_add_meta_to_simple_dict(self):
cli = JcCli() cli = JcCli()
cli.data_out = {'a': 1, 'b': 2} cli.data_out = {'a': 1, 'b': 2}

View File

@ -11,6 +11,12 @@ class MyTests(unittest.TestCase):
p = jc.lib.get_parser('arp') p = jc.lib.get_parser('arp')
self.assertIsInstance(p, ModuleType) self.assertIsInstance(p, ModuleType)
def test_lib_get_parser_broken_parser(self):
"""get_parser substitutes the disabled_parser if a parser is broken"""
broken = jc.lib.get_parser('broken_parser')
disabled = jc.lib.get_parser('disabled_parser')
self.assertIs(broken, disabled)
def test_lib_get_parser_module(self): def test_lib_get_parser_module(self):
p = jc.lib.get_parser(csv_parser) p = jc.lib.get_parser(csv_parser)
self.assertIsInstance(p, ModuleType) self.assertIsInstance(p, ModuleType)

View File

@ -65,6 +65,14 @@ class MyTests(unittest.TestCase):
""" """
self.assertEqual(jc.parsers.uptime.parse(self.osx_10_14_6_uptime, quiet=True), self.osx_10_14_6_uptime_json) self.assertEqual(jc.parsers.uptime.parse(self.osx_10_14_6_uptime, quiet=True), self.osx_10_14_6_uptime_json)
def test_uptime_busybox(self):
"""
Test 'uptime' on busybox with no user information
"""
data = '00:03:32 up 3 min, load average: 0.00, 0.00, 0.00'
expected = {"time":"00:03:32","uptime":"3 min","load_1m":0.0,"load_5m":0.0,"load_15m":0.0,"time_hour":0,"time_minute":3,"time_second":32,"uptime_days":0,"uptime_hours":0,"uptime_minutes":3,"uptime_total_seconds":180}
self.assertEqual(jc.parsers.uptime.parse(data, quiet=True), expected)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -2,7 +2,6 @@ import os
import unittest import unittest
import json import json
import jc.parsers.xml import jc.parsers.xml
import xmltodict
# fix for whether tests are run directly or via runtests.sh # fix for whether tests are run directly or via runtests.sh
try: try:
@ -10,10 +9,18 @@ try:
except: except:
from _vendor.packaging import version # type: ignore from _vendor.packaging import version # type: ignore
# check the version of installed xmltodict library
try:
import xmltodict
XMLTODICT_INSTALLED = True
XMLTODICT_0_13_0_OR_HIGHER = version.parse(xmltodict.__version__) >= version.parse('0.13.0') # type: ignore
except:
XMLTODICT_INSTALLED = False
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) THIS_DIR = os.path.dirname(os.path.abspath(__file__))
XMLTODICT_0_13_0_OR_HIGHER = version.parse(xmltodict.__version__) >= version.parse('0.13.0')
@unittest.skipIf(not XMLTODICT_INSTALLED, 'xmltodict library not installed')
class MyTests(unittest.TestCase): class MyTests(unittest.TestCase):
# input # input

View File

@ -1,36 +1,33 @@
import pprint
import re import re
import unittest import unittest
from typing import Optional from typing import Optional
from jc.parsers.xrandr import ( from jc.parsers.xrandr import (
_parse_screen,
_parse_device,
_parse_mode,
_parse_model,
_device_pattern,
_screen_pattern,
_mode_pattern,
_frequencies_pattern,
_edid_head_pattern,
_edid_line_pattern,
parse,
Mode,
Model,
Device, Device,
Screen Edid,
_Line,
LineType,
ResolutionMode,
Response,
Screen,
_device_pattern,
_frequencies_pattern,
_parse_device,
_parse_resolution_mode,
_parse_screen,
_resolution_mode_pattern,
_screen_pattern,
parse,
) )
import jc.parsers.xrandr
class XrandrTests(unittest.TestCase): class XrandrTests(unittest.TestCase):
def setUp(self):
jc.parsers.xrandr.parse_state = {}
def test_xrandr_nodata(self): def test_xrandr_nodata(self):
""" """
Test 'xrandr' with no data Test 'xrandr' with no data
""" """
self.assertEqual(parse("", quiet=True), {}) self.assertEqual(parse("", quiet=True), {"screens": []})
def test_regexes(self): def test_regexes(self):
devices = [ devices = [
@ -61,37 +58,30 @@ class XrandrTests(unittest.TestCase):
"1400x900 59.96 59.88", "1400x900 59.96 59.88",
] ]
for mode in modes: for mode in modes:
match = re.match(_mode_pattern, mode) match = re.match(_resolution_mode_pattern, mode)
self.assertIsNotNone(match) self.assertIsNotNone(match)
if match: if match:
rest = match.groupdict()["rest"] rest = match.groupdict()["rest"]
self.assertIsNotNone(re.match(_frequencies_pattern, rest)) self.assertIsNotNone(re.match(_frequencies_pattern, rest))
edid_lines = [ def test_line_categorize(self):
" EDID: ", base = "eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 309mm x 174mm"
" 00ffffffffffff000469d41901010101 ", resolution_mode = " 320x240 60.05"
" 2011010308291a78ea8585a6574a9c26 ", prop_key = " EDID:"
" 125054bfef80714f8100810f81408180 ", prop_value = " 00ffffffffffff0006af3d5700000000"
" 9500950f01019a29a0d0518422305098 ", invalid = ""
" 360098ff1000001c000000fd00374b1e ",
" 530f000a202020202020000000fc0041 ",
" 535553205657313933530a20000000ff ",
" 0037384c383032313130370a20200077 ",
]
for i in range(len(edid_lines)): self.assertEqual(LineType.Device, _Line.categorize(base).t)
line = edid_lines[i] self.assertEqual(LineType.ResolutionMode, _Line.categorize(resolution_mode).t)
if i == 0: self.assertEqual(LineType.PropKey, _Line.categorize(prop_key).t)
match = re.match(_edid_head_pattern, line) self.assertEqual(LineType.PropValue, _Line.categorize(prop_value).t)
else: with self.assertRaises(Exception):
match = re.match(_edid_line_pattern, line) _Line.categorize(invalid)
self.assertIsNotNone(match)
def test_screens(self): def test_screens(self):
sample = "Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767" sample = "Screen 0: minimum 8 x 8, current 1920 x 1080, maximum 32767 x 32767"
line = _Line.categorize(sample)
actual: Optional[Screen] = _parse_screen([sample]) actual: Optional[Screen] = _parse_screen(line)
self.assertIsNotNone(actual) self.assertIsNotNone(actual)
expected = { expected = {
@ -110,7 +100,8 @@ class XrandrTests(unittest.TestCase):
sample = ( sample = (
"Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384" "Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384"
) )
actual = _parse_screen([sample]) line = _Line.categorize(sample)
actual = _parse_screen(line)
if actual: if actual:
self.assertEqual(320, actual["minimum_width"]) self.assertEqual(320, actual["minimum_width"])
else: else:
@ -119,7 +110,8 @@ class XrandrTests(unittest.TestCase):
def test_device(self): def test_device(self):
# regex101 sample link for tests/edits https://regex101.com/r/3cHMv3/1 # regex101 sample link for tests/edits https://regex101.com/r/3cHMv3/1
sample = "eDP1 connected primary 1920x1080+0+0 left (normal left inverted right x axis y axis) 310mm x 170mm" sample = "eDP1 connected primary 1920x1080+0+0 left (normal left inverted right x axis y axis) 310mm x 170mm"
actual: Optional[Device] = _parse_device([sample]) line = _Line.categorize(sample)
actual: Optional[Device] = _parse_device(line)
expected = { expected = {
"device_name": "eDP1", "device_name": "eDP1",
@ -140,17 +132,19 @@ class XrandrTests(unittest.TestCase):
for k, v in expected.items(): for k, v in expected.items():
self.assertEqual(v, actual[k], f"Devices regex failed on {k}") self.assertEqual(v, actual[k], f"Devices regex failed on {k}")
with open("tests/fixtures/generic/xrandr_device.out", "r") as f: # with open("tests/fixtures/generic/xrandr_device.out", "r") as f:
extended_sample = f.read().splitlines() # extended_sample = f.read().splitlines()
extended_sample.reverse()
device = _parse_device(extended_sample) # device = _parse_device(extended_sample)
if device: # if device:
self.assertEqual(59.94, device["modes"][12]["frequencies"][4]["frequency"]) # self.assertEqual(
# 59.94, device["resolution_modes"][12]["frequencies"][4]["frequency"]
# )
def test_device_with_reflect(self): def test_device_with_reflect(self):
sample = "VGA-1 connected primary 1920x1080+0+0 left X and Y axis (normal left inverted right x axis y axis) 310mm x 170mm" sample = "VGA-1 connected primary 1920x1080+0+0 left X and Y axis (normal left inverted right x axis y axis) 310mm x 170mm"
actual: Optional[Device] = _parse_device([sample]) line = _Line.categorize(sample)
actual: Optional[Device] = _parse_device(line)
expected = { expected = {
"device_name": "VGA-1", "device_name": "VGA-1",
@ -183,7 +177,8 @@ class XrandrTests(unittest.TestCase):
"resolution_height": 1080, "resolution_height": 1080,
"is_high_resolution": False, "is_high_resolution": False,
} }
actual: Optional[Mode] = _parse_mode(sample_1) line = _Line.categorize(sample_1)
actual: Optional[ResolutionMode] = _parse_resolution_mode(line)
self.assertIsNotNone(actual) self.assertIsNotNone(actual)
@ -192,7 +187,8 @@ class XrandrTests(unittest.TestCase):
self.assertEqual(v, actual[k], f"mode regex failed on {k}") self.assertEqual(v, actual[k], f"mode regex failed on {k}")
sample_2 = " 1920x1080i 60.00 50.00 59.94" sample_2 = " 1920x1080i 60.00 50.00 59.94"
actual: Optional[Mode] = _parse_mode(sample_2) line = _Line.categorize(sample_2)
actual: Optional[ResolutionMode] = _parse_resolution_mode(line)
self.assertIsNotNone(actual) self.assertIsNotNone(actual)
if actual: if actual:
self.assertEqual(True, actual["is_high_resolution"]) self.assertEqual(True, actual["is_high_resolution"])
@ -205,7 +201,9 @@ class XrandrTests(unittest.TestCase):
actual = parse(txt, quiet=True) actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"])) self.assertEqual(1, len(actual["screens"]))
self.assertEqual(18, len(actual["screens"][0]["devices"][0]["modes"])) self.assertEqual(
18, len(actual["screens"][0]["devices"][0]["resolution_modes"])
)
def test_complete_2(self): def test_complete_2(self):
with open("tests/fixtures/generic/xrandr_2.out", "r") as f: with open("tests/fixtures/generic/xrandr_2.out", "r") as f:
@ -213,7 +211,9 @@ class XrandrTests(unittest.TestCase):
actual = parse(txt, quiet=True) actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"])) self.assertEqual(1, len(actual["screens"]))
self.assertEqual(38, len(actual["screens"][0]["devices"][0]["modes"])) self.assertEqual(
38, len(actual["screens"][0]["devices"][0]["resolution_modes"])
)
def test_complete_3(self): def test_complete_3(self):
with open("tests/fixtures/generic/xrandr_3.out", "r") as f: with open("tests/fixtures/generic/xrandr_3.out", "r") as f:
@ -232,84 +232,119 @@ class XrandrTests(unittest.TestCase):
actual = parse(txt, quiet=True) actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"])) self.assertEqual(1, len(actual["screens"]))
self.assertEqual(2, len(actual["screens"][0]["devices"][0]["modes"])) self.assertEqual(2, len(actual["screens"][0]["devices"][0]["resolution_modes"]))
def test_complete_5(self): def test_complete_5(self):
with open("tests/fixtures/generic/xrandr_properties.out", "r") as f: with open("tests/fixtures/generic/xrandr_properties_1.out", "r") as f:
txt = f.read() txt = f.read()
actual = parse(txt, quiet=True) actual = parse(txt, quiet=True)
self.assertEqual(1, len(actual["screens"])) self.assertEqual(1, len(actual["screens"]))
self.assertEqual(29, len(actual["screens"][0]["devices"][0]["modes"])) self.assertEqual(
38, len(actual["screens"][0]["devices"][0]["resolution_modes"])
)
def test_model(self): # def test_model(self):
asus_edid = [ # asus_edid = [
" EDID: ", # " EDID: ",
" 00ffffffffffff000469d41901010101", # " 00ffffffffffff000469d41901010101",
" 2011010308291a78ea8585a6574a9c26", # " 2011010308291a78ea8585a6574a9c26",
" 125054bfef80714f8100810f81408180", # " 125054bfef80714f8100810f81408180",
" 9500950f01019a29a0d0518422305098", # " 9500950f01019a29a0d0518422305098",
" 360098ff1000001c000000fd00374b1e", # " 360098ff1000001c000000fd00374b1e",
" 530f000a202020202020000000fc0041", # " 530f000a202020202020000000fc0041",
" 535553205657313933530a20000000ff", # " 535553205657313933530a20000000ff",
" 0037384c383032313130370a20200077", # " 0037384c383032313130370a20200077",
] # ]
asus_edid.reverse() # asus_edid.reverse()
expected = { # expected = {
"name": "ASUS VW193S", # "name": "ASUS VW193S",
"product_id": "6612", # "product_id": "6612",
"serial_number": "78L8021107", # "serial_number": "78L8021107",
} # }
actual: Optional[Model] = _parse_model(asus_edid) # actual: Optional[EdidModel] = _parse_model(asus_edid)
self.assertIsNotNone(actual) # self.assertIsNotNone(actual)
if actual: # if actual:
for k, v in expected.items(): # for k, v in expected.items():
self.assertEqual(v, actual[k], f"mode regex failed on {k}") # self.assertEqual(v, actual[k], f"mode regex failed on {k}")
generic_edid = [ # generic_edid = [
" EDID: ", # " EDID: ",
" 00ffffffffffff004ca3523100000000", # " 00ffffffffffff004ca3523100000000",
" 0014010380221378eac8959e57549226", # " 0014010380221378eac8959e57549226",
" 0f505400000001010101010101010101", # " 0f505400000001010101010101010101",
" 010101010101381d56d4500016303020", # " 010101010101381d56d4500016303020",
" 250058c2100000190000000f00000000", # " 250058c2100000190000000f00000000",
" 000000000025d9066a00000000fe0053", # " 000000000025d9066a00000000fe0053",
" 414d53554e470a204ca34154000000fe", # " 414d53554e470a204ca34154000000fe",
" 004c544e313536415432343430310018", # " 004c544e313536415432343430310018",
] # ]
generic_edid.reverse() # generic_edid.reverse()
expected = { # expected = {
"name": "Generic", # "name": "Generic",
"product_id": "12626", # "product_id": "12626",
"serial_number": "0", # "serial_number": "0",
} # }
jc.parsers.xrandr.parse_state = {} # jc.parsers.xrandr.parse_state = {}
actual: Optional[Model] = _parse_model(generic_edid) # actual: Optional[EdidModel] = _parse_model(generic_edid)
self.assertIsNotNone(actual) # self.assertIsNotNone(actual)
if actual: # if actual:
for k, v in expected.items(): # for k, v in expected.items():
self.assertEqual(v, actual[k], f"mode regex failed on {k}") # self.assertEqual(v, actual[k], f"mode regex failed on {k}")
empty_edid = [""]
actual: Optional[Model] = _parse_model(empty_edid)
self.assertIsNone(actual)
# empty_edid = [""]
# actual: Optional[EdidModel] = _parse_model(empty_edid)
# self.assertIsNone(actual)
def test_issue_490(self): def test_issue_490(self):
"""test for issue 490: https://github.com/kellyjonbrazil/jc/issues/490""" """test for issue 490: https://github.com/kellyjonbrazil/jc/issues/490"""
data_in = '''\ data_in = """\
Screen 0: minimum 1024 x 600, current 1024 x 600, maximum 1024 x 600 Screen 0: minimum 1024 x 600, current 1024 x 600, maximum 1024 x 600
default connected 1024x600+0+0 0mm x 0mm default connected 1024x600+0+0 0mm x 0mm
1024x600 0.00* 1024x600 0.00*
''' """
expected = {"screens":[{"devices":[{"modes":[{"resolution_width":1024,"resolution_height":600,"is_high_resolution":False,"frequencies":[{"frequency":0.0,"is_current":True,"is_preferred":False}]}],"is_connected":True,"is_primary":False,"device_name":"default","rotation":"normal","reflection":"normal","resolution_width":1024,"resolution_height":600,"offset_width":0,"offset_height":0,"dimension_width":0,"dimension_height":0}],"screen_number":0,"minimum_width":1024,"minimum_height":600,"current_width":1024,"current_height":600,"maximum_width":1024,"maximum_height":600}]} actual: Response = parse(data_in)
self.assertEqual(jc.parsers.xrandr.parse(data_in), expected) self.maxDiff = None
self.assertEqual(1024, actual["screens"][0]["devices"][0]["resolution_width"])
def test_issue_525(self):
self.maxDiff = None
with open("tests/fixtures/generic/xrandr_issue_525.out", "r") as f:
txt = f.read()
actual = parse(txt, quiet=True)
dp4 = actual["screens"][0]["devices"][0]["props"]["Broadcast RGB"][1] # type: ignore
# pprint.pprint(actual)
self.assertEqual("supported: Automatic, Full, Limited 16:235", dp4)
edp1_expected_keys = {
"EDID",
"EdidModel",
"scaling mode",
"Colorspace",
"max bpc",
"Broadcast RGB",
"panel orientation",
"link-status",
"CTM",
"CONNECTOR_ID",
"non-desktop",
}
actual_keys = set(actual["screens"][0]["devices"][0]["props"].keys())
self.assertSetEqual(edp1_expected_keys, actual_keys)
expected_edid_model = {
"name": "Generic",
"product_id": "22333",
"serial_number": "0",
}
self.assertDictEqual(
expected_edid_model,
actual["screens"][0]["devices"][0]["props"]["EdidModel"], # type: ignore
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -5,7 +5,14 @@ import jc.parsers.yaml
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) THIS_DIR = os.path.dirname(os.path.abspath(__file__))
try:
import ruamel.yaml
RUAMELYAML_INSTALLED = True
except:
RUAMELYAML_INSTALLED = False
@unittest.skipIf(not RUAMELYAML_INSTALLED, 'ruamel.yaml library not installed')
class MyTests(unittest.TestCase): class MyTests(unittest.TestCase):
# input # input