Project

General

Profile

Download (6.54 KB) Statistics
| Branch: | Tag: | Revision:

hydrilla-builder / src / hydrilla / util / _util.py @ f42f5c19

1
# SPDX-License-Identifier: AGPL-3.0-or-later
2

    
3
# Building Hydrilla packages.
4
#
5
# This file is part of Hydrilla
6
#
7
# Copyright (C) 2021, 2022 Wojtek Kosior
8
#
9
# This program is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU Affero General Public License as
11
# published by the Free Software Foundation, either version 3 of the
12
# License, or (at your option) any later version.
13
#
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
# GNU Affero General Public License for more details.
18
#
19
# You should have received a copy of the GNU Affero General Public License
20
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
#
22
#
23
# I, Wojtek Kosior, thereby promise not to sue for violation of this
24
# file's license. Although I request that you do not make use this code
25
# in a proprietary program, I am not going to enforce this in court.
26

    
27
# Enable using with Python 3.7.
28
from __future__ import annotations
29

    
30
import re
31
import json
32
import locale
33
import gettext
34

    
35
from pathlib import Path
36
from typing import Optional, Union
37

    
38
from jsonschema import RefResolver, Draft7Validator
39

    
40
here = Path(__file__).resolve().parent
41

    
42
class UnknownSchemaError(Exception):
43
    """
44
    Exception used to record problems with JSON documents for which not even
45
    the appropriate validation schema could be determined.
46
    """
47
    pass
48

    
49
_strip_comment_re = re.compile(r'''
50
^ # match from the beginning of each line
51
( # catch the part before '//' comment
52
  (?: # this group matches either a string or a single out-of-string character
53
    [^"/] |
54
    "
55
    (?: # this group matches any in-a-string character
56
      [^"\\] |          # match any normal character
57
      \\[^u] |          # match any escaped character like '\f' or '\n'
58
      \\u[a-fA-F0-9]{4} # match an escape
59
    )*
60
    "
61
  )*
62
)
63
# expect either end-of-line or a comment:
64
# * unterminated strings will cause matching to fail
65
# * bad comment (with '/' instead of '//') will be indicated by second group
66
#   having length 1 instead of 2 or 0
67
(//?|$)
68
''', re.VERBOSE)
69

    
70
def strip_json_comments(text: str) -> str:
71
    """
72
    Accept JSON text with optional C++-style ('//') comments and return the text
73
    with comments removed. Consecutive slashes inside strings are handled
74
    properly. A spurious single slash ('/') shall generate an error. Errors in
75
    JSON itself shall be ignored.
76
    """
77
    processed = 0
78
    stripped_text = []
79
    for line in text.split('\n'):
80
        match = _strip_comment_re.match(line)
81

    
82
        if match is None: # unterminated string
83
            # ignore this error, let json module report it
84
            stripped = line
85
        elif len(match[2]) == 1:
86
            raise json.JSONDecodeError(_('bad_comment'), text,
87
                                       processed + len(match[1]))
88
        else:
89
            stripped = match[1]
90

    
91
        stripped_text.append(stripped)
92
        processed += len(line) + 1
93

    
94
    return '\n'.join(stripped_text)
95

    
96
def normalize_version(ver: list[int]) -> list[int]:
97
    """Strip right-most zeroes from 'ver'. The original list is not modified."""
98
    new_len = 0
99
    for i, num in enumerate(ver):
100
        if num != 0:
101
            new_len = i + 1
102

    
103
    return ver[:new_len]
104

    
105
def parse_version(ver_str: str) -> list[int]:
106
    """
107
    Convert 'ver_str' into an array representation, e.g. for ver_str="4.6.13.0"
108
    return [4, 6, 13, 0].
109
    """
110
    return [int(num) for num in ver_str.split('.')]
111

    
112
def version_string(ver: list[int], rev: Optional[int]=None) -> str:
113
    """
114
    Produce version's string representation (optionally with revision), like:
115
        1.2.3-5
116
    No version normalization is performed.
117
    """
118
    return '.'.join([str(n) for n in ver]) + ('' if rev is None else f'-{rev}')
119

    
120
schemas = {}
121
for series_dir in (here.parent / 'schemas').glob('*.x'):
122
    for path in series_dir.glob("*.schema.json"):
123
        schema = json.loads(path.read_text())
124
        schemas[schema['$id']] = schema
125

    
126
common_schema_filename = 'common_definitions-1.schema.json'
127
common_schema_path = here.parent / "schemas" / common_schema_filename
128

    
129
def validator_for(schema: Union[str, dict]) -> Draft7Validator:
130
    """
131
    Prepare a validator for the provided schema.
132

    
133
    Other schemas under '../schemas' can be referenced.
134
    """
135
    if isinstance(schema, str):
136
        schema = schemas[f'https://hydrilla.koszko.org/schemas/{schema}']
137

    
138
    resolver = RefResolver(
139
        base_uri=schema['$id'],
140
        referrer=schema,
141
        handlers={'https': lambda uri: schemas[uri]}
142
    )
143

    
144
    return Draft7Validator(schema, resolver=resolver)
145

    
146
_major_version_re = re.compile(r'''
147
-
148
(?P<major>[1-9][0-9]*)
149
(?: # this repeated group matches the remaining version numbers
150
  \.
151
  (?:[1-9][0-9]*|0)
152
)*
153
\.schema\.json
154
$
155
''', re.VERBOSE)
156

    
157
def load_instance_from_file(path: Path) -> tuple[dict, Optional[int]]:
158
    """
159
    Open a file and load its contents as a JSON document (with additional
160
    '//' comments support). Then parse its "$schema" property (if present)
161
    and return a tuple of the document instance and the major number of
162
    schema version.
163

    
164
    If no schema version number can be extracted, None is used instead.
165
    """
166
    instance = json.loads(strip_json_comments(path.read_text()))
167
    major = None
168

    
169
    if type(instance) is dict and type(instance.get('$schema')) is str:
170
        match = _major_version_re.search(instance.get('$schema'))
171
        major = match and int(match.group('major'))
172

    
173
    return instance, major
174

    
175
def translation(localedir: Union[Path, str], lang: Optional[str]=None) \
176
    -> gettext.GNUTranslations:
177
    """
178
    Configure translations for domain 'hydrilla-messages' and return the object
179
    that represents them.
180

    
181
    If `lang` is set, look for translations for `lang`. Otherwise, try to
182
    determine system's default language and use that.
183
    """
184
    # https://stackoverflow.com/questions/3425294/how-to-detect-the-os-default-language-in-python
185
    # But I am not going to surrender to Microbugs' nonfree, crappy OS to test
186
    # it, to the lines inside try: may fail.
187
    if lang is None:
188
        try:
189
            from ctypes.windll import kernel32 as windll
190
            lang = locale.windows_locale[windll.GetUserDefaultUILanguage()]
191
        except:
192
            lang = locale.getdefaultlocale()[0] or 'en_US'
193

    
194
    localedir = Path(localedir)
195
    if not (localedir / lang).is_dir():
196
        lang = 'en_US'
197

    
198
    return gettext.translation('hydrilla-messages', localedir=localedir,
199
                               languages=[lang])
200

    
201
_ = translation(here.parent / 'builder' / 'locales').gettext
(2-2/2)